File size: 26,986 Bytes
73196e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0e9ff78
73196e5
 
0e9ff78
73196e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0e9ff78
73196e5
 
0e9ff78
73196e5
 
 
 
0e9ff78
73196e5
 
 
 
 
 
 
0e9ff78
73196e5
 
 
 
0e9ff78
73196e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0e9ff78
73196e5
 
0e9ff78
73196e5
0e9ff78
 
73196e5
0e9ff78
73196e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0e9ff78
 
73196e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0e9ff78
73196e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0e9ff78
73196e5
 
 
 
0e9ff78
73196e5
 
 
 
0e9ff78
73196e5
 
0e9ff78
73196e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0e9ff78
73196e5
0e9ff78
73196e5
 
0e9ff78
73196e5
 
 
 
 
 
 
 
 
 
 
 
0e9ff78
73196e5
 
 
 
0e9ff78
73196e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0e9ff78
73196e5
 
 
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
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
from lxml import etree as ET
import copy  # Để tạo bản sao sâu của rPr
import os
import traceback # Để in chi tiết lỗi

# --- Namespaces (giữ nguyên) ---
ns = {
    'a': "http://schemas.openxmlformats.org/drawingml/2006/main",
    'p': "http://schemas.openxmlformats.org/presentationml/2006/main",
    'r': "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
    'dgm': 'http://schemas.openxmlformats.org/drawingml/2006/diagram',
    'pr': 'http://schemas.openxmlformats.org/package/2006/relationships'
}

# --- Đăng ký namespace (giữ nguyên) ---
for prefix, uri in ns.items():
    if prefix != 'pr':
        ET.register_namespace(prefix, uri)


def _get_paragraph_details(p_element):
    """
    Helper function to extract merged text and the first rPr associated with text
    from a given <a:p> element. Handles text within <a:r> and <a:fld>.

    Args:
        p_element (ET.Element): The <a:p> element.

    Returns:
        tuple | None: (merged_text, first_rPr_with_text) if text exists, else None.
    """
    paragraph_text_parts = []
    first_rPr_with_text = None
    found_first_rpr = False # Cờ để chỉ tìm rPr đầu tiên một lần

    # Duyệt qua các con TRỰC TIẾP của <a:p> để xử lý <a:r> và <a:fld>
    for child_elem in p_element:
        current_rpr = None
        found_text_in_child = None

        # Trường hợp 1: Run thông thường (<a:r>)
        if child_elem.tag == f"{{{ns['a']}}}r":
            # Tìm text <a:t> bên trong run (dùng .// an toàn cho run lồng nhau nếu có)
            t_elem = child_elem.find('.//a:t', ns)
            if t_elem is not None and t_elem.text is not None:
                found_text_in_child = t_elem.text
                # Tìm rPr của run này
                current_rpr = child_elem.find('.//a:rPr', ns) # Dùng .//

        # Trường hợp 2: Field (<a:fld>)
        elif child_elem.tag == f"{{{ns['a']}}}fld":
            # Tìm text <a:t> là con TRỰC TIẾP của field
            t_elem = child_elem.find('./a:t', ns)
            if t_elem is not None and t_elem.text is not None:
                found_text_in_child = t_elem.text
                # Tìm rPr là con TRỰC TIẾP của field
                current_rpr = child_elem.find('./a:rPr', ns)

        # Xử lý nếu tìm thấy text trong child hiện tại (hoặc <a:r> hoặc <a:fld>)
        if found_text_in_child is not None:
            paragraph_text_parts.append(found_text_in_child)
            # Nếu chưa lưu rPr đầu tiên, lưu rPr của child hiện tại
            if not found_first_rpr:
                first_rPr_with_text = current_rpr # Lưu rPr tìm được (có thể là None)
                found_first_rpr = True # Đánh dấu đã tìm thấy

    # Chỉ trả về kết quả nếu paragraph thực sự có nội dung text
    if paragraph_text_parts:
        merged_text = "".join(paragraph_text_parts).strip()
        if merged_text:
            # Trả về text đã ghép và rPr đầu tiên tìm thấy (có thể là None)
            return (merged_text, first_rPr_with_text)

    return None # Không có text trong paragraph này hoặc text rỗng

# --- Hàm trích xuất chính (Trả về list các tuple chi tiết paragraph) ---
def extract_text_from_slide(slide_file):
    """
    Trích xuất chi tiết từ từng thẻ <a:p> trong file slide XML.

    Args:
        slide_file (str): Đường dẫn đến file slide XML.

    Returns:
        list: Một list các tuple, mỗi tuple có dạng:
              (paragraph_text, first_rPr_in_paragraph)
              - paragraph_text (str): Toàn bộ text trong các <a:t> con cháu
                của <a:p>, đã được ghép và strip().
              - first_rPr_in_paragraph (ET.Element | None): Phần tử <a:rPr> của
                <a:r> đầu tiên có chứa text trong <a:p> đó. Là None nếu run
                đầu tiên có text không có thẻ <a:rPr>, hoặc nếu không có text
                nào trong paragraph.
              Trả về list rỗng nếu có lỗi hoặc không tìm thấy paragraph nào có text.
    """
    # print(f"--- Bắt đầu trích xuất chi tiết từng <a:p> từ file: {slide_file} ---")
    extracted_data = [] # Danh sách kết quả cuối cùng

    if not os.path.exists(slide_file):
        print(f"Lỗi: File không tồn tại: {slide_file}")
        print(f"--- Kết thúc trích xuất file: {slide_file} (Lỗi) ---")
        return extracted_data

    try:
        tree = ET.parse(slide_file)
        root = tree.getroot()
    except ET.ParseError as e:
        print(f"Lỗi parse XML file {slide_file}: {e}")
        print(f"--- Kết thúc trích xuất file: {slide_file} (Lỗi Parse) ---")
        return extracted_data
    except Exception as e:
        print(f"Lỗi không xác định khi parse {slide_file}: {e}")
        # traceback.print_exc()
        print(f"--- Kết thúc trích xuất file: {slide_file} (Lỗi Parse không xác định) ---")
        return extracted_data

    try:
        processed_txBody_elements = set()
        elements_to_check = []

        # 1. Thu thập các container có thể chứa txBody
        for sp in root.findall('.//p:spTree/p:sp', ns): elements_to_check.append(sp)
        for grpSp in root.findall('.//p:spTree/p:grpSp', ns):
            for sp_in_grp in grpSp.findall('.//p:sp', ns): elements_to_check.append(sp_in_grp)
        for tc in root.findall('.//a:tbl//a:tc', ns): elements_to_check.append(tc)
        # Thêm tìm kiếm khác nếu cần

        # 2. Duyệt qua container, tìm txBody, rồi xử lý từng <a:p> bên trong
        for container in elements_to_check:
            txBody = container.find('./p:txBody', ns)
            if txBody is None: txBody = container.find('./a:txBody', ns)

            if txBody is not None and txBody not in processed_txBody_elements:
                 # Tìm TẤT CẢ các thẻ <a:p> là con TRỰC TIẾP của txBody này
                 paragraphs = txBody.findall('a:p', ns)
                 for p_elem in paragraphs:
                     # Gọi hàm helper để lấy chi tiết của paragraph này
                     details = _get_paragraph_details(p_elem)
                     # Nếu paragraph có nội dung text, thêm tuple vào kết quả
                     if details:
                         extracted_data.append(details)

                 processed_txBody_elements.add(txBody)

    except Exception as e:
        print(f"Lỗi khi tìm kiếm hoặc trích xuất chi tiết <a:p>: {e}")

    return extracted_data


def replace_text_in_slide(xml_file_path, list_of_translated_paragraph_data):
    """
    Thay thế văn bản trong file XML slide, ghi đè file gốc.
    *** Logic mới: ***
    - Giảm cỡ chữ đi 0.85 lần.
    - Nếu text > 20 chars: Loại bỏ định dạng bold (giữ nguyên case).
    - Nếu text <= 20 chars: Giữ nguyên định dạng bold gốc (và case).

    Args:
        xml_file_path (str): Đường dẫn file XML slide gốc (sẽ bị ghi đè).
        list_of_translated_paragraph_data (list): List các tuple
            (translated_paragraph_text, original_first_rPr_in_paragraph).
    Returns:
        bool: True nếu thành công (ghi file), False nếu có lỗi.
    """
    # print(f"\n--- Bắt đầu thay thế PARAGRAPH (ghi đè, logic length/bold) trong file: {os.path.basename(xml_file_path)} ---")
    processed_p_count = 0

    if not os.path.exists(xml_file_path):
        print(f"Lỗi: Không tìm thấy file XML nguồn '{xml_file_path}'.")
        return False

    try:
        tree = ET.parse(xml_file_path)
        root = tree.getroot()

        # --- TÌM và LỌC <a:p> THEO CÙNG LOGIC NHƯ EXTRACT ---
        paragraphs_to_modify = []
        processed_txBody_elements = set()
        elements_to_check = []
        for sp in root.findall('.//p:spTree/p:sp', ns): elements_to_check.append(sp)
        for grpSp in root.findall('.//p:spTree/p:grpSp', ns):
            for sp_in_grp in grpSp.findall('.//p:sp', ns): elements_to_check.append(sp_in_grp)
        for tc in root.findall('.//a:tbl//a:tc', ns): elements_to_check.append(tc)

        for container in elements_to_check:
            txBody = container.find('./p:txBody', ns)
            if txBody is None: txBody = container.find('./a:txBody', ns)
            if txBody is not None and txBody not in processed_txBody_elements:
                 paragraphs = txBody.findall('a:p', ns)
                 for p_elem in paragraphs:
                     has_actual_text = False
                     elements_with_text = p_elem.findall('.//a:r/a:t', ns) + p_elem.findall('.//a:fld/a:t', ns)
                     for t in elements_with_text:
                         if t.text and t.text.strip(): has_actual_text = True; break
                     if has_actual_text: paragraphs_to_modify.append(p_elem)
                 processed_txBody_elements.add(txBody)

        # --- Kiểm tra số lượng khớp ---
        num_paragraphs_found = len(paragraphs_to_modify)
        num_data_items = len(list_of_translated_paragraph_data)

        if num_paragraphs_found == 0:
            #  print(f"Thông báo [...]: Không tìm thấy <a:p> nào có text để thay thế.")
             if num_data_items > 0: print(f"Cảnh báo: Đã cung cấp {num_data_items} mục dữ liệu nhưng không có <a:p> nào để áp dụng.")
            #  print(f"--- Kết thúc xử lý (không thay đổi): {os.path.basename(xml_file_path)} ---")
             return True

        if num_paragraphs_found != num_data_items:
            print(f"CẢNH BÁO [...]: Số lượng <a:p> ({num_paragraphs_found}) KHÔNG KHỚP dữ liệu dịch ({num_data_items}).")
            num_items_to_process = min(num_paragraphs_found, num_data_items)
            print(f"=> Sẽ chỉ xử lý {num_items_to_process} mục đầu tiên.")
        else:
            num_items_to_process = num_paragraphs_found

        # --- Lặp và thực hiện thay thế ---
        for i in range(num_items_to_process):
            try:
                p_elem_to_modify = paragraphs_to_modify[i]
                translated_text, rpr_to_use_original = list_of_translated_paragraph_data[i]
                p_id = hex(id(p_elem_to_modify))

                # --- 1. Xử lý text ban đầu (chỉ strip) ---
                cleaned_translated_text = translated_text.strip() if isinstance(translated_text, str) else ""

                # --- 2. Chuẩn bị rPr cuối cùng (bắt đầu bằng copy hoặc trống) ---
                final_rpr = None
                if rpr_to_use_original is not None and ET.iselement(rpr_to_use_original) and rpr_to_use_original.tag == f"{{{ns['a']}}}rPr":
                    try:
                        final_rpr = copy.deepcopy(rpr_to_use_original)
                    except Exception as clone_e:
                        print(f"Lỗi sao chép rPr gốc cho <a:p> index {i} (ID {p_id}): {clone_e}")
                        final_rpr = ET.Element(f"{{{ns['a']}}}rPr")
                else:
                     final_rpr = ET.Element(f"{{{ns['a']}}}rPr")

                # --- 3. Luôn giảm cỡ chữ (nếu có) ---
                original_sz_str = final_rpr.get('sz')
                if original_sz_str:
                    try:
                        original_sz = int(original_sz_str)
                        new_sz = max(100, int(original_sz * 0.85))
                        final_rpr.set('sz', str(new_sz))
                    except ValueError:
                        print(f"Cảnh báo: Không thể chuyển đổi sz='{original_sz_str}' thành số nguyên cho p_id {p_id}.")

                # --- 4. Áp dụng logic độ dài cho bold (KHÔNG ĐỔI CASE) ---
                if len(cleaned_translated_text) > 10:
                    # Dài > 20: BỎ BOLD (nếu có)
                    final_rpr.attrib.pop('b', None) # Xóa thuộc tính bold
                    # print(f"Debug: Text > 20 chars for p_id {p_id}. Removed bold.")
                # else:
                    # Ngắn <= 20: Giữ lại thuộc tính 'b' gốc (đã có trong final_rpr nếu có)
                    # print(f"Debug: Text <= 20 chars for p_id {p_id}. Kept original bold.")

                # --- 5. Xóa nội dung cũ (run và field) ---
                runs_to_remove = p_elem_to_modify.findall('a:r', ns)
                fields_to_remove = p_elem_to_modify.findall('a:fld', ns)
                for elem_to_remove in runs_to_remove + fields_to_remove:
                    try: p_elem_to_modify.remove(elem_to_remove)
                    except ValueError: pass

                # --- 6. Tạo nội dung mới (nếu text không rỗng) ---
                if cleaned_translated_text:
                    new_r = ET.Element(f"{{{ns['a']}}}r")
                    new_r.insert(0, final_rpr) # Chèn rPr đã xử lý
                    new_t = ET.SubElement(new_r, f"{{{ns['a']}}}t")
                    new_t.text = cleaned_translated_text # Chèn text gốc (đã strip)
                    # Chèn run mới
                    end_para_rpr = p_elem_to_modify.find('./a:endParaRPr', ns)
                    insert_index = -1
                    if end_para_rpr is not None:
                        try: insert_index = list(p_elem_to_modify).index(end_para_rpr)
                        except ValueError: insert_index = -1
                    if insert_index != -1: p_elem_to_modify.insert(insert_index, new_r)
                    else: p_elem_to_modify.append(new_r)
                    processed_p_count += 1

            except (IndexError, ValueError, TypeError) as data_err: print(f"Lỗi lấy dữ liệu tại index {i}: {data_err}. Bỏ qua mục này.")
            except Exception as p_replace_err:
                 p_id_err = hex(id(paragraphs_to_modify[i])) if i < len(paragraphs_to_modify) else "N/A"
                 print(f"Lỗi khi xử lý thay thế cho <a:p> tại index {i} (ID {p_id_err}): {p_replace_err}")


        # --- Lưu cây XML ---
        try:
            tree.write(xml_file_path, encoding='utf-8', xml_declaration=True, pretty_print=True)
        except TypeError:
             tree.write(xml_file_path, encoding='utf-8', xml_declaration=True)
        return True

    except ET.ParseError as pe: print(f"Lỗi parse XML file '{xml_file_path}': {pe}"); return False
    except IOError as ioe: print(f"Lỗi I/O với file '{xml_file_path}': {ioe}"); return False
    except Exception as e: print(f"Lỗi nghiêm trọng: {e}"); traceback.print_exc(); return False

# --------------------------
# 2. Xử lý SmartArt
# --------------------------
def get_smartart_data_file(rels_file, base_path):
    """
    Đọc file .rels và tìm relationship có Type là diagramData,
    trả về đường dẫn đầy đủ đến file data*.xml của SmartArt.
    (Không thay đổi đáng kể)
    """
    try:
        if not os.path.exists(rels_file):
             # print(f"Thông báo: File rels không tồn tại: {rels_file}") # Có thể bỏ qua log này
             return None
        tree = ET.parse(rels_file)
        root = tree.getroot()
        # Sử dụng ns['pr']
        for rel in root.findall('pr:Relationship', ns):
            target = rel.attrib.get('Target')
            rel_type = rel.attrib.get('Type')
            # Kiểm tra Type chính xác
            if rel_type == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData' and target:
                target_fixed = target.replace("../", "")
                full_target_path = os.path.join(base_path, target_fixed) 
                absolute_path = os.path.normpath(full_target_path)
                if os.path.exists(absolute_path):
                    return absolute_path
                else:
                    print(f"Cảnh báo: Tìm thấy relationship SmartArt nhưng file target không tồn tại: {absolute_path}")
        return None
    except ET.ParseError as e:
        print(f"Lỗi parse XML file rels {rels_file}: {e}")
        return None
    except Exception as e:
        print(f"Lỗi khi xử lý file rels {rels_file}: {e}")
        # traceback.print_exc()
        return None
    

def extract_text_from_smartart(xml_file_path):
    """
    Trích xuất văn bản tổng hợp từ mỗi đoạn <a:p> có chứa text
    trong file XML SmartArt.

    Args:
        xml_file_path (str): Đường dẫn đến file XML SmartArt.

    Returns:
        list: Một list các tuple (paragraph_text, first_rPr_in_paragraph).
              paragraph_text là toàn bộ text trong các <a:t> con cháu của <a:p>.
              first_rPr_in_paragraph là element <a:rPr> của <a:r> đầu tiên
              có chứa text trong <a:p> đó. Trả về list rỗng nếu lỗi.
    """
    paragraph_data = []
    try:
        tree = ET.parse(xml_file_path)
        root = tree.getroot()

        # Tìm tất cả các đoạn <a:p> trong cây XML (thường nằm trong <dgm:txBody>)
        # Sử dụng .// để tìm ở mọi cấp độ sâu trong các cấu trúc SmartArt
        for p_elem in root.findall('.//a:p', ns):
            combined_text = ""
            first_rPr = None
            found_first_rpr_in_p = False # Cờ cho rPr đầu tiên trong đoạn p này

            # Tìm tất cả các run <a:r> bên trong đoạn <a:p> hiện tại
            for r_elem in p_elem.findall('.//a:r', ns):
                t_element = r_elem.find('.//a:t', ns) # Tìm text trong run

                if t_element is not None and t_element.text is not None:
                    current_text = t_element.text
                    combined_text += current_text # Nối text từ các run

                    # Lấy rPr của run đầu tiên có text trong đoạn p này
                    if not found_first_rpr_in_p and current_text.strip():
                        rPr_element = r_elem.find('.//a:rPr', ns)
                        first_rPr = rPr_element # Lưu trữ element rPr (có thể là None)
                        found_first_rpr_in_p = True

            # Sau khi duyệt hết các run trong <a:p>, thêm vào kết quả nếu có text
            cleaned_text = combined_text.strip()
            if cleaned_text:
                paragraph_data.append((cleaned_text, first_rPr))

    except FileNotFoundError:
        print(f"Lỗi: Không tìm thấy file XML '{xml_file_path}'.")
        return []
    except ET.ParseError as pe:
        print(f"Lỗi phân tích cú pháp XML file '{xml_file_path}': {pe}")
        return []
    except Exception as e:
        print(f"Lỗi không xác định khi trích xuất text theo đoạn từ file '{xml_file_path}': {e}")
        traceback.print_exc()
        return []

    return paragraph_data

# --- Hàm thay thế theo từng đoạn <a:p> ---
def replace_text_in_smartart(xml_file_path, list_of_translated_paragraph_data, output_xml_file_path):
    """
    Thay thế văn bản trong file XML SmartArt dựa trên dữ liệu đoạn <a:p> đã dịch.
    Mỗi mục dịch sẽ thay thế nội dung text của một <a:p> tương ứng,
    đặt toàn bộ text dịch vào một run <a:r> duy nhất với định dạng rPr được cung cấp.

    Args:
        xml_file_path (str): Đường dẫn file XML gốc.
        list_of_translated_paragraph_data (list): List các tuple
            (translated_paragraph_text, original_first_rPr_in_paragraph).
        output_xml_file_path (str): Đường dẫn file XML đầu ra.

    Returns:
        bool: True nếu thành công, False nếu lỗi.
    """
    p_index_for_data = 0 # Index để lấy dữ liệu dịch
    processed_p_count = 0 # Đếm số đoạn <a:p> đã được xử lý (thay thế)
    if not output_xml_file_path:
        output_xml_file_path = xml_file_path
    try:
        tree = ET.parse(xml_file_path)
        root = tree.getroot()

        # Tạo parent map để xóa element an toàn khi dùng findall với './/'
        parent_map = {c: p for p in root.iter() for c in p}

        # Tìm lại tất cả các <a:p> theo cùng thứ tự như khi trích xuất
        paragraphs_in_order = root.findall('.//a:p', ns)

        # Lọc ra những đoạn <a:p> mà ban đầu có chứa text để khớp với logic trích xuất
        paragraphs_to_modify = []
        for p_elem in paragraphs_in_order:
             has_actual_text = False
             for t in p_elem.findall('.//a:t', ns):
                 if t.text and t.text.strip():
                     has_actual_text = True
                     break
             if has_actual_text:
                 paragraphs_to_modify.append(p_elem)

        # Kiểm tra số lượng khớp
        if len(paragraphs_to_modify) != len(list_of_translated_paragraph_data):
            print(f"Cảnh báo [File: {os.path.basename(xml_file_path)}]: Số lượng <a:p> có text ({len(paragraphs_to_modify)}) "
                  f"không khớp số lượng dữ liệu dịch ({len(list_of_translated_paragraph_data)}). Thay thế có thể sai lệch.")
            # Quyết định số lượng sẽ xử lý
            num_items_to_process = min(len(paragraphs_to_modify), len(list_of_translated_paragraph_data))
        else:
            num_items_to_process = len(paragraphs_to_modify)


        # Duyệt qua các <a:p> cần sửa đổi
        for i in range(num_items_to_process):
            p_elem = paragraphs_to_modify[i]
            translated_text, original_first_rPr = list_of_translated_paragraph_data[p_index_for_data]
            cleaned_translated_text = translated_text.strip() if translated_text else ""

            # --- Xóa các run <a:r> cũ bên trong <a:p> này ---
            # Sử dụng .// để nhất quán với extraction, cần parent map để xóa
            runs_to_remove = p_elem.findall('.//a:r', ns)
            for r_elem in runs_to_remove:
                parent = parent_map.get(r_elem)
                if parent is not None:
                    try:
                        # Cập nhật parent map nếu cấu trúc thay đổi động (ít khả năng ở đây)
                        # parent_map = {c: p for p in root.iter() for c in p}
                        parent.remove(r_elem)
                    except ValueError:
                         pass # Bỏ qua nếu không tìm thấy để xóa
                # else: # r_elem không có parent trong map (hiếm)

            if cleaned_translated_text:
                new_r = ET.Element(f"{{{ns['a']}}}r") # Tạo run mới

                # Áp dụng rPr gốc (đã deepcopy) cho run mới
                applied_rPr = False
                if original_first_rPr is not None and ET.iselement(original_first_rPr):
                    # *** Thêm kiểm tra thẻ rPr ở đây cho an toàn ***
                    if original_first_rPr.tag == f"{{{ns['a']}}}rPr":
                        try:
                            cloned_rPr = copy.deepcopy(original_first_rPr)
                            new_r.insert(0, cloned_rPr) # Chèn rPr vào đầu run
                            applied_rPr = True
                        except Exception as clone_e:
                            print(f"Lỗi sao chép rPr cho <a:p> index {i} (data index {p_index_for_data}): {clone_e}")
                    else:
                         print(f"Cảnh báo: Thẻ rPr gốc không phải <a:rPr> cho p_elem index {i}. Thẻ: {original_first_rPr.tag}")


                if not applied_rPr:
                    ET.SubElement(new_r, f"{{{ns['a']}}}rPr") # Thêm rPr trống nếu cần

                # Thêm text vào run
                new_t = ET.SubElement(new_r, f"{{{ns['a']}}}t")
                new_t.text = cleaned_translated_text

                # --- SỬA ĐỔI QUAN TRỌNG: Chèn run mới vào đúng vị trí ---
                # Tìm phần tử <a:endParaRPr> là con TRỰC TIẾP của p_elem
                end_para_rpr = p_elem.find('./a:endParaRPr', ns)

                if end_para_rpr is not None:
                    # Nếu tìm thấy, lấy danh sách con hiện tại và tìm index của nó
                    try:
                        children_list = list(p_elem)
                        insert_index = children_list.index(end_para_rpr)
                        # Chèn run mới *ngay trước* endParaRPr
                        p_elem.insert(insert_index, new_r)
                        # print(f"Inserted new_r at index {insert_index} before endParaRPr for p_elem {i}")
                    except ValueError:
                         # Hiếm khi xảy ra nếu find() hoạt động đúng, nhưng là fallback
                         print(f"Cảnh báo: Không tìm thấy index của endParaRPr dù đã find thấy. Appending new_r cho p_elem {i}.")
                         p_elem.append(new_r)
                else:
                    # Nếu không có endParaRPr, append vào cuối là hành vi chấp nhận được
                    p_elem.append(new_r)
                    # print(f"Appended new_r (no endParaRPr found) for p_elem {i}")

            # Nếu cleaned_translated_text rỗng, đoạn <a:p> sẽ bị trống (đã xóa hết <a:r>)

            p_index_for_data += 1 # Chuyển sang dữ liệu dịch tiếp theo
            processed_p_count += 1 # Tăng số đoạn đã xử lý

        # print(f"Thông tin [File: {os.path.basename(xml_file_path)}]: Đã xử lý {processed_p_count} đoạn <a:p>.")
        if p_index_for_data < len(list_of_translated_paragraph_data):
             print(f"Cảnh báo [File: {os.path.basename(xml_file_path)}]: Còn {len(list_of_translated_paragraph_data) - p_index_for_data} "
                   f"mục dữ liệu dịch chưa được sử dụng do số lượng <a:p> không đủ.")


        # --- Lưu cây XML đã sửa đổi ---
        for prefix, uri in ns.items():
            ET.register_namespace(prefix, uri)
        tree.write(output_xml_file_path, encoding='utf-8', xml_declaration=True)
        # print(f"Đã lưu SmartArt cập nhật (theo đoạn) vào: {output_xml_file_path}")
        return True

    except FileNotFoundError:
        print(f"Lỗi: Không tìm thấy file XML nguồn '{xml_file_path}'.")
        return False
    except ET.ParseError as pe:
        print(f"Lỗi phân tích cú pháp XML file '{xml_file_path}': {pe}")
        return False
    except IOError as ioe:
         print(f"Lỗi I/O khi ghi file '{output_xml_file_path}': {ioe}")
         return False
    except Exception as e:
        print(f"Lỗi nghiêm trọng trong quá trình thay thế text SmartArt (theo đoạn) file '{xml_file_path}': {e}")
        traceback.print_exc()
        return False