mintlee commited on
Commit
73196e5
·
1 Parent(s): 0666452

update pptx

Browse files
db/__pycache__/mongodb.cpython-310.pyc CHANGED
Binary files a/db/__pycache__/mongodb.cpython-310.pyc and b/db/__pycache__/mongodb.cpython-310.pyc differ
 
db/mongodb.py CHANGED
@@ -39,75 +39,9 @@ def save_file_to_mongodb(uploaded_file, db_name="ppt", collection_name="root_fil
39
  print(f"✅ File '{file_name}' đã được lưu vào '{collection_name}' với ID: {file_id}")
40
 
41
  client.close()
42
- return file_id
43
 
44
- def delete_pptx_from_mongodb(file_id, db_name="ppt", collection_name="root_file"):
45
- """
46
- Xóa file PowerPoint khỏi MongoDB theo ID.
47
-
48
- :param file_id: ID của file cần xóa (chuỗi hoặc ObjectId)
49
- :param db_name: Tên database trong MongoDB
50
- :param collection_name: Tên collection GridFS
51
- """
52
- # Kết nối đến MongoDB
53
- client = MongoClient("mongodb+srv://admin:1highbar456@cluster0.equkm.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0")
54
- db = client[db_name]
55
- fs = gridfs.GridFS(db, collection=collection_name)
56
-
57
- try:
58
- # Chuyển đổi ID nếu cần
59
- if not isinstance(file_id, ObjectId):
60
- file_id = ObjectId(file_id)
61
-
62
- # Kiểm tra file có tồn tại không
63
- if fs.exists(file_id):
64
- fs.delete(file_id)
65
- print(f"✅ Đã xóa file với ID: {file_id}")
66
- else:
67
- print(f"⚠️ Không tìm thấy file với ID: {file_id}")
68
- except Exception as e:
69
- print(f"❌ Lỗi khi xóa file: {e}")
70
-
71
- client.close()
72
-
73
- def download_pptx_from_mongodb(file_id, save_path, save_name, db_name="ppt", collection_name="root_file"):
74
- """
75
- Tải file PowerPoint từ MongoDB GridFS và lưu về máy.
76
-
77
- :param file_id: ID của file cần tải (dạng chuỗi hoặc ObjectId)
78
- :param save_path: Đường dẫn đến thư mục sẽ lưu file (VD: 'D:/output')
79
- :param save_name: Tên file khi lưu (VD: 'my_presentation.pptx')
80
- :param db_name: Tên database trong MongoDB (mặc định: 'ppt')
81
- :param collection_name: Tên collection GridFS (mặc định: 'root_file')
82
- """
83
- # Đảm bảo thư mục lưu file tồn tại
84
- os.makedirs(save_path, exist_ok=True)
85
-
86
- # Tạo đường dẫn đầy đủ cho file
87
- full_file_path = os.path.join(save_path, save_name)
88
-
89
- # Kết nối đến MongoDB
90
- client = MongoClient("mongodb+srv://admin:1highbar456@cluster0.equkm.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0")
91
- db = client[db_name]
92
- fs = gridfs.GridFS(db, collection=collection_name)
93
-
94
- try:
95
- # Chuyển đổi ID nếu cần
96
- if not isinstance(file_id, ObjectId):
97
- file_id = ObjectId(file_id)
98
 
99
- # Lấy dữ liệu file từ GridFS
100
- file_data = fs.get(file_id)
101
-
102
- # Ghi dữ liệu ra file
103
- with open(full_file_path, "wb") as f:
104
- f.write(file_data.read())
105
-
106
- print(f"✅ File đã được tải về: {full_file_path}")
107
- except Exception as e:
108
- print(f"❌ Lỗi khi tải file: {e}")
109
- finally:
110
- client.close()
111
 
112
  def save_xml_to_gridfs(xml_content, file_name, db_name="ppt", collection_name="original_xml"):
113
  """
 
39
  print(f"✅ File '{file_name}' đã được lưu vào '{collection_name}' với ID: {file_id}")
40
 
41
  client.close()
42
+ return file_id, file_name
43
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  def save_xml_to_gridfs(xml_content, file_name, db_name="ppt", collection_name="original_xml"):
47
  """
home.py CHANGED
@@ -11,7 +11,7 @@ st.write(
11
  "Ứng dụng này giúp bạn dịch tài liệu một cách nhanh chóng và chính xác."
12
  )
13
 
14
- st.header("📌 Hướng dẫn sử dụng")
15
  st.write("1. Truy cập [trang upload](#).")
16
  st.write("2. Chọn tệp tin cần dịch (hỗ trợ .docx, .txt, .pdf).")
17
  st.write("3. Chọn ngôn ngữ muốn dịch sang.")
@@ -25,7 +25,7 @@ if to_upload:
25
  st.switch_page("pages/upload.py") # Điều hướng đến trang upload
26
 
27
 
28
- st.header("🛠️ Patch Note")
29
  st.subheader("25/03/2025")
30
  st.write("1. Đã hoàn thành file Word, Excel")
31
  st.write("2. Đang tiến hành file PPTX (tiến độ 90% đã có thể dùng thử)")
 
11
  "Ứng dụng này giúp bạn dịch tài liệu một cách nhanh chóng và chính xác."
12
  )
13
 
14
+ st.header("📌 Hướng dẫn sử dụng (đọc Patch Notes)")
15
  st.write("1. Truy cập [trang upload](#).")
16
  st.write("2. Chọn tệp tin cần dịch (hỗ trợ .docx, .txt, .pdf).")
17
  st.write("3. Chọn ngôn ngữ muốn dịch sang.")
 
25
  st.switch_page("pages/upload.py") # Điều hướng đến trang upload
26
 
27
 
28
+ st.header("🛠️ Patch Notes")
29
  st.subheader("25/03/2025")
30
  st.write("1. Đã hoàn thành file Word, Excel")
31
  st.write("2. Đang tiến hành file PPTX (tiến độ 90% đã có thể dùng thử)")
pages/upload.py CHANGED
@@ -1,11 +1,7 @@
1
  import streamlit as st
2
  import google.generativeai as genai
3
  from db.mongodb import save_file_to_mongodb, fetch_file_from_mongodb, detect_file_type
4
- from powerpoint.xml_handling import (
5
- extract_text_from_xml, update_xml_with_translated_text_mongodb, ppt_to_xml_mongodb
6
- )
7
- from translate.translator import translate_text_dict
8
- from powerpoint.pptx_object import create_translated_ppt
9
  from excel.excel_translate import translate_xlsx, translate_csv
10
  from word.word_translate import translate_docx_from_mongodb
11
  import dotenv
@@ -18,22 +14,24 @@ genai.configure(api_key=os.getenv("GEMINI_API_KEY"))
18
  st.title("Translate Your File Easily! 🌍")
19
 
20
  uploaded_file = st.file_uploader("📂 Chọn file để dịch")
21
- target_lang = st.selectbox("🌐 Chọn ngôn ngữ", ["english", "vietnamese"])
 
 
22
 
23
  def process_file(file, file_type):
24
  progress_bar = st.progress(0)
25
- file_id = save_file_to_mongodb(uploaded_file=file, db_name=file_type.lower(), collection_name="root_file")
26
  progress_bar.progress(20)
27
  st.write(f"📂 File ID: {file_id}")
28
 
29
  if file_type == "PPTX":
30
- xml_file_id = ppt_to_xml_mongodb(file_id)
31
- progress_bar.progress(40)
32
- text_dict = extract_text_from_xml(file_id=xml_file_id)
33
- translated_dict = translate_text_dict(text_dict, target_lang=target_lang)
34
- progress_bar.progress(60)
35
- final_xml_id = update_xml_with_translated_text_mongodb(xml_file_id, translated_dict)
36
- final_id = create_translated_ppt("pptx", file_id, final_xml_id, "final_file")
37
  elif file_type == "Excel":
38
  final_id = translate_xlsx(file_id = file_id, target_lang = target_lang)
39
  elif file_type == "CSV":
 
1
  import streamlit as st
2
  import google.generativeai as genai
3
  from db.mongodb import save_file_to_mongodb, fetch_file_from_mongodb, detect_file_type
4
+ from powerpoint.pptx import translate_pptx
 
 
 
 
5
  from excel.excel_translate import translate_xlsx, translate_csv
6
  from word.word_translate import translate_docx_from_mongodb
7
  import dotenv
 
14
  st.title("Translate Your File Easily! 🌍")
15
 
16
  uploaded_file = st.file_uploader("📂 Chọn file để dịch")
17
+ source_lang = st.selectbox("🌐 Chọn ngôn ngữ của tài liệu", ["english", "vietnamese"])
18
+ target_lang = st.selectbox("🌐 Chọn ngôn ngữ muốn dịch sang", ["english", "vietnamese"])
19
+
20
 
21
  def process_file(file, file_type):
22
  progress_bar = st.progress(0)
23
+ file_id, file_name = save_file_to_mongodb(uploaded_file=file, db_name=file_type.lower(), collection_name="root_file")
24
  progress_bar.progress(20)
25
  st.write(f"📂 File ID: {file_id}")
26
 
27
  if file_type == "PPTX":
28
+ final_id = translate_pptx(file_id, file_name, source_lang='vn', target_lang='en', slides_per_batch=5)
29
+ # progress_bar.progress(40)
30
+ # text_dict = extract_text_from_xml(file_id=xml_file_id)
31
+ # translated_dict = translate_text_dict(text_dict, target_lang=target_lang)
32
+ # progress_bar.progress(60)
33
+ # final_xml_id = update_xml_with_translated_text_mongodb(xml_file_id, translated_dict)
34
+ # final_id = create_translated_ppt("pptx", file_id, final_xml_id, "final_file")
35
  elif file_type == "Excel":
36
  final_id = translate_xlsx(file_id = file_id, target_lang = target_lang)
37
  elif file_type == "CSV":
powerpoint/__pycache__/pptx.cpython-310.pyc ADDED
Binary file (4.32 kB). View file
 
powerpoint/__pycache__/xml_handling.cpython-310.pyc CHANGED
Binary files a/powerpoint/__pycache__/xml_handling.cpython-310.pyc and b/powerpoint/__pycache__/xml_handling.cpython-310.pyc differ
 
powerpoint/pptx.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import zipfile
3
+ import shutil
4
+ from pptx import Presentation
5
+ from utils.utils import unzip_office_file, translate_text, preprocess_text, postprocess_text
6
+ from powerpoint.xml_handling import *
7
+ from pymongo import MongoClient
8
+ import gridfs
9
+ from bson import ObjectId
10
+ from io import BytesIO
11
+
12
+ def create_pptx_and_store_in_mongodb(temp_dir, pptx_filename):
13
+ """
14
+ Tạo file PPTX từ thư mục chứa nội dung đã giải nén và lưu vào MongoDB mà không lưu file trên ổ cứng.
15
+ """
16
+ pptx_buffer = BytesIO()
17
+
18
+ with zipfile.ZipFile(pptx_buffer, 'w', zipfile.ZIP_DEFLATED) as zipf:
19
+ for root_dir, _, files in os.walk(temp_dir):
20
+ for file in files:
21
+ file_path = os.path.join(root_dir, file)
22
+ arcname = os.path.relpath(file_path, temp_dir)
23
+ zipf.write(file_path, arcname)
24
+
25
+ pptx_buffer.seek(0)
26
+
27
+ client = MongoClient("mongodb+srv://admin:1highbar456@cluster0.equkm.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0")
28
+ db = client['pptx']
29
+ fs = gridfs.GridFS(db, collection='final_file')
30
+
31
+ file_id = fs.put(pptx_buffer, filename=pptx_filename)
32
+
33
+ print(f"PPTX đã được lưu vào MongoDB với ID: {file_id}")
34
+ client.close()
35
+
36
+ return file_id
37
+
38
+ def translate_and_replace_pptx(xml_folder, file_name, source_lang='vn', target_lang='en', slides_per_batch=5):
39
+ slides_dir = os.path.join(xml_folder, "ppt/slides")
40
+ all_slides = sorted([f for f in os.listdir(slides_dir)
41
+ if f.startswith("slide") and f.endswith(".xml")],
42
+ key=lambda x: int(x[5:-4]))
43
+
44
+ # Xử lý theo từng batch slide
45
+ for i in range(0, len(all_slides), slides_per_batch):
46
+ batch_slides = all_slides[i:i + slides_per_batch]
47
+ slide_text_mapping = {}
48
+ smartart_text_mapping = {}
49
+
50
+ for slide_file in batch_slides:
51
+ slide_index = int(slide_file[5:-4])
52
+ slide_path = os.path.join(slides_dir, slide_file)
53
+ slide_text_mapping[slide_index] = extract_text_from_slide(slide_path) # Lấy list các tuple (text, rPr)
54
+
55
+ # Xử lý SmartArt qua file .rels của slide
56
+ rels_file = os.path.join(xml_folder, "ppt/slides/_rels", slide_file + ".rels")
57
+ base_path = os.path.join(xml_folder, "ppt")
58
+ smartart_data_path = get_smartart_data_file(rels_file, base_path)
59
+ if smartart_data_path:
60
+ smartart_text_mapping[slide_index] = extract_text_from_smartart(smartart_data_path) # Lấy list các tuple (text, rPr)
61
+
62
+
63
+ # Gộp text để dịch theo batch, giữ lại rPr
64
+ combined_slide_text_list = []
65
+ for slide_index in sorted(slide_text_mapping.keys()):
66
+ combined_slide_text_list.extend(slide_text_mapping[slide_index])
67
+
68
+ combined_smartart_text_list = []
69
+ for slide_index in sorted(smartart_text_mapping.keys()):
70
+ combined_smartart_text_list.extend(smartart_text_mapping[slide_index])
71
+
72
+ # Tách text ra khỏi tuple để dịch
73
+ slide_texts_to_translate = [text for text, _ in combined_slide_text_list]
74
+ smartart_texts_to_translate = [text for text, _ in combined_smartart_text_list]
75
+
76
+ # Dịch văn bản slide và SmartArt
77
+ combined_slide_text_string = preprocess_text(slide_texts_to_translate)
78
+ combined_smartart_text_string = preprocess_text(smartart_texts_to_translate)
79
+
80
+ translated_slide_string = translate_text(combined_slide_text_string, source_lang, target_lang)
81
+ translated_smartart_string = translate_text(combined_smartart_text_string, source_lang, target_lang)
82
+
83
+ # Postprocess để có list các văn bản đã dịch
84
+ translated_slide_texts = postprocess_text(translated_slide_string)
85
+ translated_smartart_texts = postprocess_text(translated_smartart_string)
86
+
87
+ # **Quan trọng:** Tạo danh sách tuple (translated_text, rPr)
88
+ translated_slide_data = []
89
+ for i, (original_text, rPr) in enumerate(combined_slide_text_list):
90
+ if i < len(translated_slide_texts):
91
+ translated_slide_data.append((translated_slide_texts[i], rPr))
92
+ else:
93
+ translated_slide_data.append(("", rPr)) # Trường hợp không đủ translated text
94
+
95
+ translated_smartart_data = []
96
+ for i, (original_text, rPr) in enumerate(combined_smartart_text_list):
97
+ if i < len(translated_smartart_texts):
98
+ translated_smartart_data.append((translated_smartart_texts[i], rPr))
99
+ else:
100
+ translated_smartart_data.append(("", rPr)) # Trường hợp không đủ translated text
101
+
102
+ # Thay thế văn bản trong slide
103
+ slide_index = 0
104
+ for slide_index in sorted(slide_text_mapping.keys()):
105
+ slide_file = f"slide{slide_index}.xml"
106
+ slide_path = os.path.join(slides_dir, slide_file)
107
+ num_texts = len(slide_text_mapping[slide_index])
108
+ replace_data = translated_slide_data[:num_texts]
109
+ replace_text_in_slide(slide_path, replace_data) # truyền vào danh sách (translated_text, rPr)
110
+ translated_slide_data = translated_slide_data[num_texts:] # Cập nhật danh sách cho slide tiếp theo
111
+
112
+ # Thay thế văn bản trong SmartArt
113
+ for slide_index in sorted(smartart_text_mapping.keys()):
114
+ rels_file = os.path.join(xml_folder, "ppt/slides/_rels", f"slide{slide_index}.xml.rels")
115
+ base_path = os.path.join(xml_folder, "ppt")
116
+ smartart_data_path = get_smartart_data_file(rels_file, base_path)
117
+ if smartart_data_path:
118
+ num_texts = len(smartart_text_mapping[slide_index])
119
+ replace_data = translated_smartart_data[:num_texts]
120
+ replace_text_in_smartart(smartart_data_path, replace_data, None) # truyền vào danh sách (translated_text, rPr)
121
+ translated_smartart_data = translated_smartart_data[num_texts:] # Cập nhật danh sách cho slide tiếp theo
122
+
123
+ file_id = create_pptx_and_store_in_mongodb(xml_folder, file_name)
124
+ return file_id
125
+
126
+ def translate_pptx(pptx_id, file_name, source_lang='vn', target_lang='en', slides_per_batch=5):
127
+ client = MongoClient("mongodb+srv://admin:1highbar456@cluster0.equkm.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0")
128
+ db = client['pptx']
129
+ fs = gridfs.GridFS(db, collection='root_file')
130
+
131
+ ppt_file = fs.get(pptx_id)
132
+ prs = BytesIO(ppt_file.read())
133
+
134
+ xml_folder = unzip_office_file(prs)
135
+ file_id = translate_and_replace_pptx(xml_folder, file_name, source_lang, target_lang, slides_per_batch=slides_per_batch)
136
+ shutil.rmtree(xml_folder)
137
+
138
+ return file_id
139
+
powerpoint/xml_handling.py CHANGED
@@ -1,377 +1,538 @@
1
- import xml.etree.ElementTree as ET
2
- from xml.dom import minidom
3
- import json
4
- from typing import Dict, List
5
- from concurrent.futures import ThreadPoolExecutor
6
- from pptx import Presentation
7
- from pptx.enum.shapes import MSO_SHAPE_TYPE
8
- from powerpoint.pptx_object import get_table_properties, get_shape_properties
9
- from pymongo import MongoClient
10
- import gridfs
11
- from bson import ObjectId
12
- from io import BytesIO
13
-
14
-
15
- gemini_api = "AIzaSyDtBIjTSfbvuEsobNwjtdyi9gVpDrCaWPM"
16
-
17
- def extract_text_from_group(group_shape, slide_number, shape_index, slide_element):
18
- """Extracts text from shapes within a group, only adding the group if it contains text."""
19
- group_element = ET.SubElement(slide_element, "group_element")
20
- group_element.set("shape_index", str(shape_index))
21
- group_element.set("group_name", group_shape.name) # Add group name
22
-
23
- group_has_text = False # Flag to track if the group contains any text
24
-
25
- for i, shape in enumerate(group_shape.shapes):
26
- if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
27
- # Recursively check nested groups, and update group_has_text
28
- if extract_text_from_group(shape, slide_number, i, group_element):
29
- group_has_text = True
30
- elif shape.shape_type == MSO_SHAPE_TYPE.TABLE:
31
- table_element = ET.SubElement(group_element, "table_element")
32
- table_element.set("shape_index", str(i))
33
- table_data = get_table_properties(shape.table)
34
- props_element = ET.SubElement(table_element, "properties")
35
- props_element.text = json.dumps(table_data, indent=2)
36
- group_has_text = True
37
- elif hasattr(shape, "text_frame") and shape.text_frame:
38
- text_element = ET.SubElement(group_element, "text_element")
39
- text_element.set("shape_index", str(i))
40
- shape_data = get_shape_properties(shape)
41
- props_element = ET.SubElement(text_element, "properties")
42
- props_element.text = json.dumps(shape_data, indent=2)
43
- if shape_data.get("text") or (
44
- "paragraphs" in shape_data
45
- and any(p.get("text") for p in shape_data["paragraphs"])
46
- ):
47
- group_has_text = True
48
-
49
- # Only keep the group element if it contains text
50
- if not group_has_text:
51
- slide_element.remove(group_element)
52
- return False
53
- return True
54
-
55
- def extract_text_from_slide(slide, slide_number, translate=False):
56
- """Extract all text elements from a slide."""
57
- slide_element = ET.Element("slide")
58
- slide_element.set("number", str(slide_number))
59
-
60
- for shape_index, shape in enumerate(slide.shapes):
61
- if shape.shape_type == MSO_SHAPE_TYPE.GROUP:
62
- extract_text_from_group(shape, slide_number, shape_index, slide_element)
63
- elif shape.shape_type == MSO_SHAPE_TYPE.TABLE:
64
- table_element = ET.SubElement(slide_element, "table_element")
65
- table_element.set("shape_index", str(shape_index))
66
- table_data = get_table_properties(shape.table)
67
- props_element = ET.SubElement(table_element, "properties")
68
- props_element.text = json.dumps(table_data, indent=2)
69
- elif hasattr(shape, "text"):
70
- text_element = ET.SubElement(slide_element, "text_element")
71
- text_element.set("shape_index", str(shape_index))
72
- shape_data = get_shape_properties(shape)
73
- props_element = ET.SubElement(text_element, "properties")
74
- props_element.text = json.dumps(shape_data, indent=2)
75
- return slide_element
76
-
77
- def ppt_to_xml_mongodb(ppt_file_id: str, db_name="pptx"):
78
  """
79
- Chuyển PowerPoint từ MongoDB thành XML lưu vào MongoDB.
 
80
 
81
- :param ppt_file_id: ID của file PPT gốc trong MongoDB (original_pptx)
82
- :param db_name: Tên database MongoDB
83
- :return: ID của file XML trong MongoDB (original_xml)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  """
85
- # Kết nối MongoDB
86
- client = MongoClient(
87
- "mongodb+srv://admin:1highbar456@cluster0.equkm.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0",
88
- connectTimeoutMS=60000, # 60 giây thay vì 20 giây
89
- serverSelectionTimeoutMS=60000, # Chờ phản hồi lâu hơn
90
- socketTimeoutMS=60000, # Tăng thời gian chờ socket
91
- tls=True,
92
- tlsAllowInvalidCertificates=True # Giữ kết nối lâu hơn
93
- )
94
- db = client[db_name]
95
-
96
- fs_ppt = gridfs.GridFS(db, collection="root_file") # PPT gốc
97
- fs_xml = gridfs.GridFS(db, collection="original_xml") # XML lưu trữ
98
 
99
- try:
100
- # Lấy file PPT từ MongoDB
101
- if not isinstance(ppt_file_id, ObjectId):
102
- ppt_file_id = ObjectId(ppt_file_id)
103
- ppt_file = fs_ppt.get(ppt_file_id)
104
- prs = Presentation(BytesIO(ppt_file.read()))
105
-
106
- # Tạo XML
107
- root = ET.Element("presentation")
108
- root.set("file_name", ppt_file.filename)
109
-
110
- with ThreadPoolExecutor(max_workers=4) as executor:
111
- future_to_slide = {
112
- executor.submit(extract_text_from_slide, slide, slide_number): slide_number
113
- for slide_number, slide in enumerate(prs.slides, 1)
114
- }
115
- for future in future_to_slide:
116
- slide_number = future_to_slide[future]
117
- try:
118
- slide_element = future.result()
119
- root.append(slide_element)
120
- except Exception as e:
121
- print(f"Error processing slide {slide_number}: {str(e)}")
122
-
123
- xml_str = minidom.parseString(ET.tostring(root)).toprettyxml(indent=" ")
124
-
125
- # Lưu XML vào MongoDB
126
- xml_output = BytesIO(xml_str.encode("utf-8"))
127
- file_name = ppt_file.filename.replace(".pptx", ".xml")
128
- xml_file_id = fs_xml.put(xml_output, filename=file_name)
129
-
130
- print(f"✅ XML đã được lưu vào MongoDB (original_xml) với file_id: {xml_file_id}")
131
- client.close()
132
-
133
- return xml_file_id
134
 
 
 
 
 
 
 
 
135
  except Exception as e:
136
- print(f"Lỗi khi chuyển PPT sang XML: {str(e)}")
137
- return None
138
- finally:
139
- client.close()
140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
 
 
142
 
 
143
 
144
- def extract_text_from_xml(file_id=None, filename=None, db_name="pptx", collection_name="original_xml") -> Dict[str, List[str]]:
145
- """
146
- Tải XML từ MongoDB và trích xuất văn bản từ các slide.
147
 
148
- :param file_id: ID của file trong MongoDB (dạng ObjectId hoặc string)
149
- :param filename: Tên file cần tìm trong MongoDB (VD: "file.xml")
150
- :param db_name: Tên database MongoDB
151
- :param collection_name: Tên collection GridFS
152
- :return: Dictionary {slide_number: [text1, text2, ...]}
153
  """
154
- # Kết nối MongoDB
155
- client = MongoClient("mongodb+srv://admin:1highbar456@cluster0.equkm.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0")
156
- db = client[db_name]
157
- fs = gridfs.GridFS(db, collection=collection_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
  try:
160
- # Tìm file theo file_id hoặc filename
161
- if file_id:
162
- if not isinstance(file_id, ObjectId):
163
- file_id = ObjectId(file_id)
164
- file_data = fs.get(file_id)
165
- elif filename:
166
- file_data = fs.find_one({"filename": filename})
167
- if not file_data:
168
- print(f"❌ Không tìm thấy file '{filename}' trong MongoDB!")
169
- return {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  else:
171
- print("❌ Cần cung cấp 'file_id' hoặc 'filename' để tải file.")
172
- return {}
173
-
174
- # Đọc nội dung XML từ MongoDB
175
- xml_content = file_data.read().decode("utf-8")
176
- # print(f"✅ xml_content: {xml_content}")
177
- # Chuyển đổi thành cây XML
178
- root = ET.fromstring(xml_content)
179
- slide_texts = {}
180
-
181
- # Duyệt qua từng slide
182
- for slide in root.findall("slide"):
183
- slide_number = slide.get("number")
184
- texts = []
185
- # Helper function to extract text recursively
186
- def extract_text_recursive(element):
187
- if element.tag == "text_element":
188
- props = element.find("properties")
189
- if props is not None and props.text:
190
- try:
191
- shape_data = json.loads(props.text)
192
- # Handle both direct 'text' and paragraph-based text
193
- if 'text' in shape_data:
194
- texts.append(shape_data['text'])
195
- elif 'paragraphs' in shape_data:
196
- for paragraph in shape_data['paragraphs']:
197
- if 'text' in paragraph:
198
- texts.append(paragraph['text'])
199
- #Also extract run level text
200
- elif 'runs' in paragraph:
201
- for run in paragraph['runs']:
202
- if 'text' in run:
203
- texts.append(run['text'])
204
-
205
-
206
- except json.JSONDecodeError:
207
- pass # Ignore if JSON is invalid
208
-
209
- elif element.tag == "table_element":
210
- props = element.find("properties")
211
- if props is not None and props.text:
212
- try:
213
- table_data = json.loads(props.text)
214
- for row in table_data.get("cells", []):
215
- for cell in row:
216
- texts.append(cell.get("text", ""))
217
- except json.JSONDecodeError:
218
- pass # Ignore if JSON is invalid
219
-
220
- # Recursively process children of group_element
221
- elif element.tag == "group_element":
222
- for child in element:
223
- extract_text_recursive(child)
224
-
225
- # Iterate through all direct children of the slide
226
- for child in slide:
227
- extract_text_recursive(child)
228
-
229
- slide_texts[str(slide_number)] = texts # Ensure slide number is a string
230
- print(slide_texts)
231
- return slide_texts
232
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  except Exception as e:
234
- print(f"Lỗi khi xử lý XML: {e}")
235
- return {}
236
- finally:
237
- client.close()
238
-
239
-
240
-
241
-
242
-
243
-
244
- def adjust_size(original_text, translated_text, data_container):
245
- """Adjust font size if translated text is significantly longer."""
246
-
247
- if not original_text or not translated_text:
248
- return
249
-
250
- original_len = len(original_text)
251
- translated_len = len(translated_text)
252
- length_ratio = translated_len / original_len if original_len >0 else 1 # Avoid division by 0
253
-
254
- if length_ratio > 1.5: # Adjust threshold as needed
255
- if 'paragraphs' in data_container:
256
- for paragraph in data_container['paragraphs']:
257
- if 'runs' in paragraph:
258
- for run in paragraph['runs']:
259
- if run.get('font') and run['font'].get('size'):
260
- run['font']['size'] = max(6, int(run['font']['size'] * 0.8))
261
-
262
- elif 'font' in data_container and data_container['font'].get('size'):
263
- data_container['font']['size'] = max(6, int(data_container['font']['size'] * 0.8))
264
 
 
 
 
 
265
 
 
 
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
 
268
 
269
- def update_xml_with_translated_text_mongodb(file_id: str, translated_dict: Dict[str, List[str]], db_name="pptx"):
 
270
  """
271
- Tải XML từ MongoDB (collection original_xml), cập nhật nội dung dịch, lưu lại vào collection final_xml.
272
-
273
- :param file_id: ID của file trong MongoDB (original_xml)
274
- :param translated_dict: Dictionary {slide_number: [translated_text1, translated_text2, ...]}
275
- :param db_name: Tên database MongoDB
 
 
 
 
 
 
 
276
  """
277
- # Kết nối MongoDB
278
- client = MongoClient("mongodb+srv://admin:1highbar456@cluster0.equkm.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0")
279
- db = client[db_name]
280
-
281
- fs_original = gridfs.GridFS(db, collection="original_xml") # Lấy file từ original_xml
282
- fs_final = gridfs.GridFS(db, collection="final_xml") # Lưu file vào final_xml
283
-
284
  try:
285
- # Tải file từ MongoDB (original_xml)
286
- if not isinstance(file_id, ObjectId):
287
- file_id = ObjectId(file_id)
288
- file_data = fs_original.get(file_id)
289
- xml_content = file_data.read().decode("utf-8")
290
-
291
- # Chuyển đổi XML string thành cây XML
292
- root = ET.fromstring(xml_content)
293
-
294
- # Cập nhật nội dung dịch
295
- for slide in root.findall("slide"):
296
- slide_num = slide.get("number")
297
- if slide_num in translated_dict:
298
- translated_texts = translated_dict[slide_num]
299
- text_index = 0 # Keep track of the current translated text
300
-
301
- def update_element_recursive(element):
302
- nonlocal text_index # Access and modify the outer scope's index
303
-
304
- if element.tag == "text_element":
305
- props = element.find("properties")
306
- if props is not None and props.text:
307
- try:
308
- shape_data = json.loads(props.text)
309
- original_text = ""
310
-
311
- # Handle direct text and paragraph-based text
312
- if 'text' in shape_data:
313
- original_text = shape_data['text']
314
- if text_index < len(translated_texts):
315
- shape_data['text'] = translated_texts[text_index]
316
- adjust_size(original_text, translated_texts[text_index], shape_data)
317
- text_index += 1
318
- elif 'paragraphs' in shape_data:
319
- for paragraph in shape_data['paragraphs']:
320
- if 'text' in paragraph:
321
- original_text = paragraph['text']
322
- if text_index < len(translated_texts):
323
- paragraph['text'] = translated_texts[text_index]
324
- adjust_size(original_text, translated_texts[text_index], paragraph)
325
- text_index += 1
326
- elif 'runs' in paragraph:
327
- for run in paragraph['runs']:
328
- if 'text' in run:
329
- original_text = run['text']
330
- if text_index < len(translated_texts):
331
- run['text'] = translated_texts[text_index]
332
- adjust_size(original_text, translated_texts[text_index], run)
333
- text_index += 1
334
- props.text = json.dumps(shape_data, indent=2)
335
- except json.JSONDecodeError:
336
- print(f"JSONDecodeError in text_element on slide {slide_num}")
337
-
338
- elif element.tag == "table_element":
339
- props = element.find("properties")
340
- if props is not None and props.text:
341
- try:
342
- table_data = json.loads(props.text)
343
- for row in table_data.get("cells", []):
344
- for cell in row:
345
- original_text = cell.get('text', '')
346
- if text_index < len(translated_texts):
347
- cell['text'] = translated_texts[text_index]
348
- adjust_size(original_text, translated_texts[text_index], cell)
349
- text_index += 1
350
- props.text = json.dumps(table_data, indent=2)
351
- except json.JSONDecodeError:
352
- print(f"JSONDecodeError in table_element on slide {slide_num}")
353
-
354
- elif element.tag == "group_element":
355
- print("Group element found")
356
- for child in element:
357
- update_element_recursive(child) # Recursively process children
358
-
359
- # Start the recursive update from the slide's direct children
360
- for child in slide:
361
- update_element_recursive(child)
362
-
363
- # Chuyển XML thành chuỗi và làm đẹp định dạng
364
- updated_xml_str = minidom.parseString(ET.tostring(root)).toprettyxml(indent=" ")
365
-
366
- # Lưu file cập nhật vào MongoDB (final_xml)
367
- new_file_id = fs_final.put(updated_xml_str.encode("utf-8"), filename=f"{file_data.filename}")
368
- print(f"✅ XML đã cập nhật được lưu vào MongoDB (final_xml) với file_id: {new_file_id}")
369
-
370
- return new_file_id
371
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  except Exception as e:
373
- print(f"Lỗi khi cập nhật XML: {e}")
374
- return None
375
- finally:
376
- client.close()
377
-
 
1
+ from lxml import etree as ET
2
+ import copy # Để tạo bản sao sâu của rPr
3
+ import os
4
+ import traceback # Để in chi tiết lỗi
5
+
6
+ # --- Namespaces (giữ nguyên) ---
7
+ ns = {
8
+ 'a': "http://schemas.openxmlformats.org/drawingml/2006/main",
9
+ 'p': "http://schemas.openxmlformats.org/presentationml/2006/main",
10
+ 'r': "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
11
+ 'dgm': 'http://schemas.openxmlformats.org/drawingml/2006/diagram',
12
+ 'pr': 'http://schemas.openxmlformats.org/package/2006/relationships'
13
+ }
14
+
15
+ # --- Đăng ký namespace (giữ nguyên) ---
16
+ for prefix, uri in ns.items():
17
+ if prefix != 'pr':
18
+ ET.register_namespace(prefix, uri)
19
+
20
+
21
+ def _get_paragraph_details(p_element):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  """
23
+ Helper function to extract merged text and the first rPr associated with text
24
+ from a given <a:p> element. Handles text within <a:r> and <a:fld>.
25
 
26
+ Args:
27
+ p_element (ET.Element): The <a:p> element.
28
+
29
+ Returns:
30
+ tuple | None: (merged_text, first_rPr_with_text) if text exists, else None.
31
+ """
32
+ paragraph_text_parts = []
33
+ first_rPr_with_text = None
34
+ found_first_rpr = False # Cờ để chỉ tìm rPr đầu tiên một lần
35
+
36
+ # Duyệt qua các con TRỰC TIẾP của <a:p> để xử lý <a:r> và <a:fld>
37
+ for child_elem in p_element:
38
+ current_rpr = None
39
+ found_text_in_child = None
40
+
41
+ # Trường hợp 1: Run thông thường (<a:r>)
42
+ if child_elem.tag == f"{{{ns['a']}}}r":
43
+ # Tìm text <a:t> bên trong run (dùng .// an toàn cho run lồng nhau nếu có)
44
+ t_elem = child_elem.find('.//a:t', ns)
45
+ if t_elem is not None and t_elem.text is not None:
46
+ found_text_in_child = t_elem.text
47
+ # Tìm rPr của run này
48
+ current_rpr = child_elem.find('.//a:rPr', ns) # Dùng .//
49
+
50
+ # Trường hợp 2: Field (<a:fld>)
51
+ elif child_elem.tag == f"{{{ns['a']}}}fld":
52
+ # Tìm text <a:t> là con TRỰC TIẾP của field
53
+ t_elem = child_elem.find('./a:t', ns)
54
+ if t_elem is not None and t_elem.text is not None:
55
+ found_text_in_child = t_elem.text
56
+ # Tìm rPr là con TRỰC TIẾP của field
57
+ current_rpr = child_elem.find('./a:rPr', ns)
58
+
59
+ # Xử lý nếu tìm thấy text trong child hiện tại (hoặc <a:r> hoặc <a:fld>)
60
+ if found_text_in_child is not None:
61
+ paragraph_text_parts.append(found_text_in_child)
62
+ # Nếu chưa lưu rPr đầu tiên, lưu rPr của child hiện tại
63
+ if not found_first_rpr:
64
+ first_rPr_with_text = current_rpr # Lưu rPr tìm được (có thể là None)
65
+ found_first_rpr = True # Đánh dấu đã tìm thấy
66
+
67
+ # Chỉ trả về kết quả nếu paragraph thực sự có nội dung text
68
+ if paragraph_text_parts:
69
+ merged_text = "".join(paragraph_text_parts).strip()
70
+ if merged_text:
71
+ # Trả về text đã ghép và rPr đầu tiên tìm thấy (có thể là None)
72
+ return (merged_text, first_rPr_with_text)
73
+
74
+ return None # Không có text trong paragraph này hoặc text rỗng
75
+
76
+ # --- Hàm trích xuất chính (Trả về list các tuple chi tiết paragraph) ---
77
+ def extract_text_from_slide(slide_file):
78
+ """
79
+ Trích xuất chi tiết từ từng thẻ <a:p> trong file slide XML.
80
+
81
+ Args:
82
+ slide_file (str): Đường dẫn đến file slide XML.
83
+
84
+ Returns:
85
+ list: Một list các tuple, mỗi tuple có dạng:
86
+ (paragraph_text, first_rPr_in_paragraph)
87
+ - paragraph_text (str): Toàn bộ text trong các <a:t> con cháu
88
+ của <a:p>, đã được ghép và strip().
89
+ - first_rPr_in_paragraph (ET.Element | None): Phần tử <a:rPr> của
90
+ <a:r> đầu tiên có chứa text trong <a:p> đó. Là None nếu run
91
+ đầu tiên có text không có thẻ <a:rPr>, hoặc nếu không có text
92
+ nào trong paragraph.
93
+ Trả về list rỗng nếu có lỗi hoặc không tìm thấy paragraph nào có text.
94
  """
95
+ # print(f"--- Bắt đầu trích xuất chi tiết từng <a:p> từ file: {slide_file} ---")
96
+ extracted_data = [] # Danh sách kết quả cuối cùng
 
 
 
 
 
 
 
 
 
 
 
97
 
98
+ if not os.path.exists(slide_file):
99
+ print(f"Lỗi: File không tồn tại: {slide_file}")
100
+ print(f"--- Kết thúc trích xuất file: {slide_file} (Lỗi) ---")
101
+ return extracted_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
+ try:
104
+ tree = ET.parse(slide_file)
105
+ root = tree.getroot()
106
+ except ET.ParseError as e:
107
+ print(f"Lỗi parse XML file {slide_file}: {e}")
108
+ print(f"--- Kết thúc trích xuất file: {slide_file} (Lỗi Parse) ---")
109
+ return extracted_data
110
  except Exception as e:
111
+ print(f"Lỗi không xác định khi parse {slide_file}: {e}")
112
+ # traceback.print_exc()
113
+ print(f"--- Kết thúc trích xuất file: {slide_file} (Lỗi Parse không xác định) ---")
114
+ return extracted_data
115
 
116
+ try:
117
+ processed_txBody_elements = set()
118
+ elements_to_check = []
119
+
120
+ # 1. Thu thập các container có thể chứa txBody
121
+ for sp in root.findall('.//p:spTree/p:sp', ns): elements_to_check.append(sp)
122
+ for grpSp in root.findall('.//p:spTree/p:grpSp', ns):
123
+ for sp_in_grp in grpSp.findall('.//p:sp', ns): elements_to_check.append(sp_in_grp)
124
+ for tc in root.findall('.//a:tbl//a:tc', ns): elements_to_check.append(tc)
125
+ # Thêm tìm kiếm khác nếu cần
126
+
127
+ # 2. Duyệt qua container, tìm txBody, rồi xử lý từng <a:p> bên trong
128
+ for container in elements_to_check:
129
+ txBody = container.find('./p:txBody', ns)
130
+ if txBody is None: txBody = container.find('./a:txBody', ns)
131
+
132
+ if txBody is not None and txBody not in processed_txBody_elements:
133
+ # Tìm TẤT CẢ các thẻ <a:p> là con TRỰC TIẾP của txBody này
134
+ paragraphs = txBody.findall('a:p', ns)
135
+ for p_elem in paragraphs:
136
+ # Gọi hàm helper để lấy chi tiết của paragraph này
137
+ details = _get_paragraph_details(p_elem)
138
+ # Nếu paragraph có nội dung text, thêm tuple vào kết quả
139
+ if details:
140
+ extracted_data.append(details)
141
+
142
+ processed_txBody_elements.add(txBody)
143
 
144
+ except Exception as e:
145
+ print(f"Lỗi khi tìm kiếm hoặc trích xuất chi tiết <a:p>: {e}")
146
 
147
+ return extracted_data
148
 
 
 
 
149
 
150
+ def replace_text_in_slide(xml_file_path, list_of_translated_paragraph_data):
 
 
 
 
151
  """
152
+ Thay thế văn bản trong file XML slide, ghi đè file gốc.
153
+ *** Logic mới: ***
154
+ - Giảm cỡ chữ đi 0.85 lần.
155
+ - Nếu text > 20 chars: Loại bỏ định dạng bold (giữ nguyên case).
156
+ - Nếu text <= 20 chars: Giữ nguyên định dạng bold gốc (và case).
157
+
158
+ Args:
159
+ xml_file_path (str): Đường dẫn file XML slide gốc (sẽ bị ghi đè).
160
+ list_of_translated_paragraph_data (list): List các tuple
161
+ (translated_paragraph_text, original_first_rPr_in_paragraph).
162
+ Returns:
163
+ bool: True nếu thành công (ghi file), False nếu có lỗi.
164
+ """
165
+ # print(f"\n--- Bắt đầu thay thế PARAGRAPH (ghi đè, logic length/bold) trong file: {os.path.basename(xml_file_path)} ---")
166
+ processed_p_count = 0
167
+
168
+ if not os.path.exists(xml_file_path):
169
+ print(f"Lỗi: Không tìm thấy file XML nguồn '{xml_file_path}'.")
170
+ return False
171
 
172
  try:
173
+ tree = ET.parse(xml_file_path)
174
+ root = tree.getroot()
175
+
176
+ # --- TÌM và LỌC <a:p> THEO CÙNG LOGIC NHƯ EXTRACT ---
177
+ paragraphs_to_modify = []
178
+ processed_txBody_elements = set()
179
+ elements_to_check = []
180
+ for sp in root.findall('.//p:spTree/p:sp', ns): elements_to_check.append(sp)
181
+ for grpSp in root.findall('.//p:spTree/p:grpSp', ns):
182
+ for sp_in_grp in grpSp.findall('.//p:sp', ns): elements_to_check.append(sp_in_grp)
183
+ for tc in root.findall('.//a:tbl//a:tc', ns): elements_to_check.append(tc)
184
+
185
+ for container in elements_to_check:
186
+ txBody = container.find('./p:txBody', ns)
187
+ if txBody is None: txBody = container.find('./a:txBody', ns)
188
+ if txBody is not None and txBody not in processed_txBody_elements:
189
+ paragraphs = txBody.findall('a:p', ns)
190
+ for p_elem in paragraphs:
191
+ has_actual_text = False
192
+ elements_with_text = p_elem.findall('.//a:r/a:t', ns) + p_elem.findall('.//a:fld/a:t', ns)
193
+ for t in elements_with_text:
194
+ if t.text and t.text.strip(): has_actual_text = True; break
195
+ if has_actual_text: paragraphs_to_modify.append(p_elem)
196
+ processed_txBody_elements.add(txBody)
197
+
198
+ # --- Kiểm tra số lượng khớp ---
199
+ num_paragraphs_found = len(paragraphs_to_modify)
200
+ num_data_items = len(list_of_translated_paragraph_data)
201
+
202
+ if num_paragraphs_found == 0:
203
+ # print(f"Thông báo [...]: Không tìm thấy <a:p> nào có text để thay thế.")
204
+ 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.")
205
+ # print(f"--- Kết thúc xử lý (không thay đổi): {os.path.basename(xml_file_path)} ---")
206
+ return True
207
+
208
+ if num_paragraphs_found != num_data_items:
209
+ 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}).")
210
+ num_items_to_process = min(num_paragraphs_found, num_data_items)
211
+ print(f"=> Sẽ chỉ xử lý {num_items_to_process} mục đầu tiên.")
212
  else:
213
+ num_items_to_process = num_paragraphs_found
214
+
215
+ # --- Lặp và thực hiện thay thế ---
216
+ for i in range(num_items_to_process):
217
+ try:
218
+ p_elem_to_modify = paragraphs_to_modify[i]
219
+ translated_text, rpr_to_use_original = list_of_translated_paragraph_data[i]
220
+ p_id = hex(id(p_elem_to_modify))
221
+
222
+ # --- 1. Xử lý text ban đầu (chỉ strip) ---
223
+ cleaned_translated_text = translated_text.strip() if isinstance(translated_text, str) else ""
224
+
225
+ # --- 2. Chuẩn bị rPr cuối cùng (bắt đầu bằng copy hoặc trống) ---
226
+ final_rpr = None
227
+ 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":
228
+ try:
229
+ final_rpr = copy.deepcopy(rpr_to_use_original)
230
+ except Exception as clone_e:
231
+ print(f"Lỗi sao chép rPr gốc cho <a:p> index {i} (ID {p_id}): {clone_e}")
232
+ final_rpr = ET.Element(f"{{{ns['a']}}}rPr")
233
+ else:
234
+ final_rpr = ET.Element(f"{{{ns['a']}}}rPr")
235
+
236
+ # --- 3. Luôn giảm cỡ chữ (nếu có) ---
237
+ original_sz_str = final_rpr.get('sz')
238
+ if original_sz_str:
239
+ try:
240
+ original_sz = int(original_sz_str)
241
+ new_sz = max(100, int(original_sz * 0.85))
242
+ final_rpr.set('sz', str(new_sz))
243
+ except ValueError:
244
+ 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}.")
245
+
246
+ # --- 4. Áp dụng logic độ dài cho bold (KHÔNG ĐỔI CASE) ---
247
+ if len(cleaned_translated_text) > 10:
248
+ # Dài > 20: BỎ BOLD (nếu có)
249
+ final_rpr.attrib.pop('b', None) # Xóa thuộc tính bold
250
+ # print(f"Debug: Text > 20 chars for p_id {p_id}. Removed bold.")
251
+ # else:
252
+ # Ngắn <= 20: Giữ lại thuộc tính 'b' gốc (đã có trong final_rpr nếu có)
253
+ # print(f"Debug: Text <= 20 chars for p_id {p_id}. Kept original bold.")
254
+
255
+ # --- 5. Xóa nội dung cũ (run và field) ---
256
+ runs_to_remove = p_elem_to_modify.findall('a:r', ns)
257
+ fields_to_remove = p_elem_to_modify.findall('a:fld', ns)
258
+ for elem_to_remove in runs_to_remove + fields_to_remove:
259
+ try: p_elem_to_modify.remove(elem_to_remove)
260
+ except ValueError: pass
261
+
262
+ # --- 6. Tạo nội dung mới (nếu text không rỗng) ---
263
+ if cleaned_translated_text:
264
+ new_r = ET.Element(f"{{{ns['a']}}}r")
265
+ new_r.insert(0, final_rpr) # Chèn rPr đã xử lý
266
+ new_t = ET.SubElement(new_r, f"{{{ns['a']}}}t")
267
+ new_t.text = cleaned_translated_text # Chèn text gốc (đã strip)
268
+ # Chèn run mới
269
+ end_para_rpr = p_elem_to_modify.find('./a:endParaRPr', ns)
270
+ insert_index = -1
271
+ if end_para_rpr is not None:
272
+ try: insert_index = list(p_elem_to_modify).index(end_para_rpr)
273
+ except ValueError: insert_index = -1
274
+ if insert_index != -1: p_elem_to_modify.insert(insert_index, new_r)
275
+ else: p_elem_to_modify.append(new_r)
276
+ processed_p_count += 1
277
+
278
+ 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.")
279
+ except Exception as p_replace_err:
280
+ p_id_err = hex(id(paragraphs_to_modify[i])) if i < len(paragraphs_to_modify) else "N/A"
281
+ print(f"Lỗi khi xử lý thay thế cho <a:p> tại index {i} (ID {p_id_err}): {p_replace_err}")
282
+
283
+
284
+ # --- Lưu cây XML ---
285
+ try:
286
+ tree.write(xml_file_path, encoding='utf-8', xml_declaration=True, pretty_print=True)
287
+ except TypeError:
288
+ tree.write(xml_file_path, encoding='utf-8', xml_declaration=True)
289
+ return True
290
+
291
+ except ET.ParseError as pe: print(f"Lỗi parse XML file '{xml_file_path}': {pe}"); return False
292
+ except IOError as ioe: print(f"Lỗi I/O với file '{xml_file_path}': {ioe}"); return False
293
+ except Exception as e: print(f"Lỗi nghiêm trọng: {e}"); traceback.print_exc(); return False
294
+
295
+ # --------------------------
296
+ # 2. Xử lý SmartArt
297
+ # --------------------------
298
+ def get_smartart_data_file(rels_file, base_path):
299
+ """
300
+ Đọc file .rels và tìm relationship có Type là diagramData,
301
+ trả về đường dẫn đầy đủ đến file data*.xml của SmartArt.
302
+ (Không thay đổi đáng kể)
303
+ """
304
+ try:
305
+ if not os.path.exists(rels_file):
306
+ # print(f"Thông báo: File rels không tồn tại: {rels_file}") # Có thể bỏ qua log này
307
+ return None
308
+ tree = ET.parse(rels_file)
309
+ root = tree.getroot()
310
+ # Sử dụng ns['pr']
311
+ for rel in root.findall('pr:Relationship', ns):
312
+ target = rel.attrib.get('Target')
313
+ rel_type = rel.attrib.get('Type')
314
+ # Kiểm tra Type chính xác
315
+ if rel_type == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData' and target:
316
+ target_fixed = target.replace("../", "")
317
+ full_target_path = os.path.join(base_path, target_fixed)
318
+ absolute_path = os.path.normpath(full_target_path)
319
+ if os.path.exists(absolute_path):
320
+ return absolute_path
321
+ else:
322
+ print(f"Cảnh báo: Tìm thấy relationship SmartArt nhưng file target không tồn tại: {absolute_path}")
323
+ return None
324
+ except ET.ParseError as e:
325
+ print(f"Lỗi parse XML file rels {rels_file}: {e}")
326
+ return None
327
  except Exception as e:
328
+ print(f"Lỗi khi xử lý file rels {rels_file}: {e}")
329
+ # traceback.print_exc()
330
+ return None
331
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
 
333
+ def extract_text_from_smartart(xml_file_path):
334
+ """
335
+ Trích xuất văn bản tổng hợp từ mỗi đoạn <a:p> có chứa text
336
+ trong file XML SmartArt.
337
 
338
+ Args:
339
+ xml_file_path (str): Đường dẫn đến file XML SmartArt.
340
 
341
+ Returns:
342
+ list: Một list các tuple (paragraph_text, first_rPr_in_paragraph).
343
+ paragraph_text là toàn bộ text trong các <a:t> con cháu của <a:p>.
344
+ first_rPr_in_paragraph là element <a:rPr> của <a:r> đầu tiên
345
+ có chứa text trong <a:p> đó. Trả về list rỗng nếu lỗi.
346
+ """
347
+ paragraph_data = []
348
+ try:
349
+ tree = ET.parse(xml_file_path)
350
+ root = tree.getroot()
351
+
352
+ # Tìm tất cả các đoạn <a:p> trong cây XML (thường nằm trong <dgm:txBody>)
353
+ # Sử dụng .// để tìm ở mọi cấp độ sâu trong các cấu trúc SmartArt
354
+ for p_elem in root.findall('.//a:p', ns):
355
+ combined_text = ""
356
+ first_rPr = None
357
+ found_first_rpr_in_p = False # Cờ cho rPr đầu tiên trong đoạn p này
358
+
359
+ # Tìm tất cả các run <a:r> bên trong đoạn <a:p> hiện tại
360
+ for r_elem in p_elem.findall('.//a:r', ns):
361
+ t_element = r_elem.find('.//a:t', ns) # Tìm text trong run
362
+
363
+ if t_element is not None and t_element.text is not None:
364
+ current_text = t_element.text
365
+ combined_text += current_text # Nối text từ các run
366
+
367
+ # Lấy rPr của run đầu tiên có text trong đoạn p này
368
+ if not found_first_rpr_in_p and current_text.strip():
369
+ rPr_element = r_elem.find('.//a:rPr', ns)
370
+ first_rPr = rPr_element # Lưu trữ element rPr (có thể là None)
371
+ found_first_rpr_in_p = True
372
+
373
+ # Sau khi duyệt hết các run trong <a:p>, thêm vào kết quả nếu có text
374
+ cleaned_text = combined_text.strip()
375
+ if cleaned_text:
376
+ paragraph_data.append((cleaned_text, first_rPr))
377
+
378
+ except FileNotFoundError:
379
+ print(f"Lỗi: Không tìm thấy file XML '{xml_file_path}'.")
380
+ return []
381
+ except ET.ParseError as pe:
382
+ print(f"Lỗi phân tích cú pháp XML file '{xml_file_path}': {pe}")
383
+ return []
384
+ except Exception as e:
385
+ print(f"Lỗi không xác định khi trích xuất text theo đoạn từ file '{xml_file_path}': {e}")
386
+ traceback.print_exc()
387
+ return []
388
 
389
+ return paragraph_data
390
 
391
+ # --- Hàm thay thế theo từng đoạn <a:p> ---
392
+ def replace_text_in_smartart(xml_file_path, list_of_translated_paragraph_data, output_xml_file_path):
393
  """
394
+ Thay thế văn bản trong file XML SmartArt dựa trên dữ liệu đoạn <a:p> đã dịch.
395
+ Mỗi mục dịch sẽ thay thế nội dung text của một <a:p> tương ứng,
396
+ đặ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.
397
+
398
+ Args:
399
+ xml_file_path (str): Đường dẫn file XML gốc.
400
+ list_of_translated_paragraph_data (list): List các tuple
401
+ (translated_paragraph_text, original_first_rPr_in_paragraph).
402
+ output_xml_file_path (str): Đường dẫn file XML đầu ra.
403
+
404
+ Returns:
405
+ bool: True nếu thành công, False nếu lỗi.
406
  """
407
+ p_index_for_data = 0 # Index để lấy dữ liệu dịch
408
+ processed_p_count = 0 # Đếm số đoạn <a:p> đã được xử lý (thay thế)
409
+ if not output_xml_file_path:
410
+ output_xml_file_path = xml_file_path
 
 
 
411
  try:
412
+ tree = ET.parse(xml_file_path)
413
+ root = tree.getroot()
414
+
415
+ # Tạo parent map để xóa element an toàn khi dùng findall với './/'
416
+ parent_map = {c: p for p in root.iter() for c in p}
417
+
418
+ # Tìm lại tất cả các <a:p> theo cùng thứ tự như khi trích xuất
419
+ paragraphs_in_order = root.findall('.//a:p', ns)
420
+
421
+ # 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
422
+ paragraphs_to_modify = []
423
+ for p_elem in paragraphs_in_order:
424
+ has_actual_text = False
425
+ for t in p_elem.findall('.//a:t', ns):
426
+ if t.text and t.text.strip():
427
+ has_actual_text = True
428
+ break
429
+ if has_actual_text:
430
+ paragraphs_to_modify.append(p_elem)
431
+
432
+ # Kiểm tra số lượng khớp
433
+ if len(paragraphs_to_modify) != len(list_of_translated_paragraph_data):
434
+ print(f"Cảnh báo [File: {os.path.basename(xml_file_path)}]: Số lượng <a:p> có text ({len(paragraphs_to_modify)}) "
435
+ 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.")
436
+ # Quyết định số lượng sẽ xử lý
437
+ num_items_to_process = min(len(paragraphs_to_modify), len(list_of_translated_paragraph_data))
438
+ else:
439
+ num_items_to_process = len(paragraphs_to_modify)
440
+
441
+
442
+ # Duyệt qua các <a:p> cần sửa đổi
443
+ for i in range(num_items_to_process):
444
+ p_elem = paragraphs_to_modify[i]
445
+ translated_text, original_first_rPr = list_of_translated_paragraph_data[p_index_for_data]
446
+ cleaned_translated_text = translated_text.strip() if translated_text else ""
447
+
448
+ # --- Xóa các run <a:r> cũ bên trong <a:p> này ---
449
+ # Sử dụng .// để nhất quán với extraction, cần parent map để xóa
450
+ runs_to_remove = p_elem.findall('.//a:r', ns)
451
+ for r_elem in runs_to_remove:
452
+ parent = parent_map.get(r_elem)
453
+ if parent is not None:
454
+ try:
455
+ # Cập nhật parent map nếu cấu trúc thay đổi động (ít khả năng ở đây)
456
+ # parent_map = {c: p for p in root.iter() for c in p}
457
+ parent.remove(r_elem)
458
+ except ValueError:
459
+ pass # Bỏ qua nếu không tìm thấy để xóa
460
+ # else: # r_elem không có parent trong map (hiếm)
461
+
462
+ if cleaned_translated_text:
463
+ new_r = ET.Element(f"{{{ns['a']}}}r") # Tạo run mới
464
+
465
+ # Áp dụng rPr gốc (đã deepcopy) cho run mới
466
+ applied_rPr = False
467
+ if original_first_rPr is not None and ET.iselement(original_first_rPr):
468
+ # *** Thêm kiểm tra thẻ rPr ở đây cho an toàn ***
469
+ if original_first_rPr.tag == f"{{{ns['a']}}}rPr":
470
+ try:
471
+ cloned_rPr = copy.deepcopy(original_first_rPr)
472
+ new_r.insert(0, cloned_rPr) # Chèn rPr vào đầu run
473
+ applied_rPr = True
474
+ except Exception as clone_e:
475
+ print(f"Lỗi sao chép rPr cho <a:p> index {i} (data index {p_index_for_data}): {clone_e}")
476
+ else:
477
+ 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}")
478
+
479
+
480
+ if not applied_rPr:
481
+ ET.SubElement(new_r, f"{{{ns['a']}}}rPr") # Thêm rPr trống nếu cần
482
+
483
+ # Thêm text vào run
484
+ new_t = ET.SubElement(new_r, f"{{{ns['a']}}}t")
485
+ new_t.text = cleaned_translated_text
486
+
487
+ # --- SỬA ĐỔI QUAN TRỌNG: Chèn run mới vào đúng vị trí ---
488
+ # Tìm phần tử <a:endParaRPr> là con TRỰC TIẾP của p_elem
489
+ end_para_rpr = p_elem.find('./a:endParaRPr', ns)
490
+
491
+ if end_para_rpr is not None:
492
+ # Nếu tìm thấy, lấy danh sách con hiện tại và tìm index của nó
493
+ try:
494
+ children_list = list(p_elem)
495
+ insert_index = children_list.index(end_para_rpr)
496
+ # Chèn run mới *ngay trước* endParaRPr
497
+ p_elem.insert(insert_index, new_r)
498
+ # print(f"Inserted new_r at index {insert_index} before endParaRPr for p_elem {i}")
499
+ except ValueError:
500
+ # Hiếm khi xảy ra nếu find() hoạt động đúng, nhưng là fallback
501
+ 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}.")
502
+ p_elem.append(new_r)
503
+ else:
504
+ # Nếu không có endParaRPr, append vào cuối là hành vi chấp nhận được
505
+ p_elem.append(new_r)
506
+ # print(f"Appended new_r (no endParaRPr found) for p_elem {i}")
507
+
508
+ # Nếu cleaned_translated_text rỗng, đoạn <a:p> sẽ bị trống (đã xóa hết <a:r>)
509
+
510
+ p_index_for_data += 1 # Chuyển sang dữ liệu dịch tiếp theo
511
+ processed_p_count += 1 # Tăng số đoạn đã xử lý
512
+
513
+ # print(f"Thông tin [File: {os.path.basename(xml_file_path)}]: Đã xử lý {processed_p_count} đoạn <a:p>.")
514
+ if p_index_for_data < len(list_of_translated_paragraph_data):
515
+ 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} "
516
+ f"mục dữ liệu dịch chưa được sử dụng do số lượng <a:p> không đủ.")
517
+
518
+
519
+ # --- Lưu cây XML đã sửa đổi ---
520
+ for prefix, uri in ns.items():
521
+ ET.register_namespace(prefix, uri)
522
+ tree.write(output_xml_file_path, encoding='utf-8', xml_declaration=True)
523
+ # print(f"Đã lưu SmartArt cập nhật (theo đoạn) vào: {output_xml_file_path}")
524
+ return True
525
+
526
+ except FileNotFoundError:
527
+ print(f"Lỗi: Không tìm thấy file XML nguồn '{xml_file_path}'.")
528
+ return False
529
+ except ET.ParseError as pe:
530
+ print(f"Lỗi phân tích cú pháp XML file '{xml_file_path}': {pe}")
531
+ return False
532
+ except IOError as ioe:
533
+ print(f"Lỗi I/O khi ghi file '{output_xml_file_path}': {ioe}")
534
+ return False
535
  except Exception as e:
536
+ print(f"Lỗi nghiêm trọng trong quá trình thay thế text SmartArt (theo đoạn) file '{xml_file_path}': {e}")
537
+ traceback.print_exc()
538
+ return False
 
 
test.ipynb CHANGED
The diff for this file is too large to render. See raw diff
 
utils/__pycache__/utils.cpython-310.pyc ADDED
Binary file (8.07 kB). View file
 
utils/utils.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import zipfile
3
+ import google.generativeai as genai
4
+ import tempfile
5
+ import io
6
+ import json
7
+
8
+ genai.configure(api_key="AIzaSyBH8O5IfqYrJ5wtWnmUC21IfMjzJCrTm3I")
9
+
10
+
11
+
12
+ def unzip_office_file(pptx_file: io.BytesIO):
13
+ """
14
+ Giải nén nội dung từ file PPTX (dạng BytesIO) vào thư mục tạm thời.
15
+ Trả về đường dẫn thư mục chứa nội dung đã giải nén và tên file gốc (không có đuôi .pptx).
16
+ """
17
+ # Tạo thư mục tạm để lưu nội dung giải nén
18
+ output_dir = tempfile.mkdtemp(prefix="pptx_extract_")
19
+
20
+ # Giải nén nội dung từ file PPTX (BytesIO)
21
+ with zipfile.ZipFile(pptx_file, 'r') as zip_ref:
22
+ zip_ref.extractall(output_dir)
23
+
24
+ return output_dir
25
+
26
+
27
+ def translate_single_text(text, source_lang='English', target_lang="Vietnamese"):
28
+ if not text:
29
+ return "" # Bỏ qua nếu chuỗi rỗng
30
+
31
+ try:
32
+ model = genai.GenerativeModel('gemini-2.0-flash') # Sử dụng model từ code gốc nếu hoạt động tốt
33
+
34
+ # --- Prompt đơn giản chỉ yêu cầu dịch thuật ---
35
+ system_prompt_simple = f"""You are a translation engine.
36
+ Translate the following text accurately from {source_lang} to {target_lang}.
37
+ Provide *only* the translated text as a single string.
38
+ Do NOT add any extra formatting, delimiters like '#', introductory phrases, or explanations."""
39
+
40
+ user_prompt = f"Source language: {source_lang}. Target language: {target_lang}. Text to translate: {text}"
41
+ full_prompt = system_prompt_simple.strip() + "\n\n" + user_prompt.strip()
42
+
43
+ response = model.generate_content(
44
+ contents=full_prompt,
45
+ generation_config={
46
+ 'temperature': 0.7, # Nhiệt độ phù hợp cho dịch thuật (có thể điều chỉnh)
47
+ 'top_p': 1,
48
+ 'top_k': 1,
49
+ }
50
+ )
51
+ translated_text = response.text.strip()
52
+ return translated_text
53
+
54
+ except Exception as e:
55
+ print(f"Lỗi trong quá trình dịch (translate_single_text): {e}")
56
+ return "" # Trả về chuỗi rỗng nếu có lỗi
57
+
58
+
59
+ def preprocess_text(text_list):
60
+ """
61
+ Converts a list of strings into a dictionary where keys are the
62
+ list indices (int) and values are the strings.
63
+ """
64
+ if not isinstance(text_list, list):
65
+ return {}
66
+ if not text_list:
67
+ return {}
68
+ text_dict = {index: text for index, text in enumerate(text_list)}
69
+ return text_dict
70
+
71
+ def translate_text(text_dict, source_lang='English', target_lang="Vietnamese"):
72
+ """
73
+ Translates the values of a dictionary {index: text} using an LLM.
74
+ It uses an intermediate JSON string format for reliable LLM interaction.
75
+ Returns a dictionary {index: translated_text} with the same keys.
76
+ """
77
+ if not isinstance(text_dict, dict):
78
+ print("Warning: translate_text_dict expected a dict, received:", type(text_dict))
79
+ return {}
80
+ if not text_dict:
81
+ return {}
82
+
83
+ # --- Internal Helper: Convert Dictionary to JSON String for LLM ---
84
+ def _dict_to_json_string(d):
85
+ json_compatible = {str(k): v for k, v in d.items()}
86
+ try:
87
+ return json.dumps(json_compatible, ensure_ascii=False, separators=(',',':'))
88
+ except Exception as e:
89
+ print(f"Internal Error (_dict_to_json_string): {e}")
90
+ return "{}"
91
+
92
+ # --- Internal Helper: Convert LLM's JSON String Response to Dictionary ---
93
+ def _json_string_to_dict(s):
94
+ res_dict = {}
95
+ if not s or not isinstance(s, str): return {}
96
+ try:
97
+ raw = json.loads(s)
98
+ if not isinstance(raw, dict):
99
+ print(f"Internal Warning (_json_string_to_dict): LLM response is not a JSON object: {s}")
100
+ return {}
101
+ for k_str, v in raw.items():
102
+ try:
103
+ res_dict[int(k_str)] = v
104
+ except ValueError:
105
+ print(f"Internal Warning (_json_string_to_dict): Non-integer key '{k_str}' in LLM response.")
106
+ except json.JSONDecodeError as e:
107
+ print(f"Internal Error (_json_string_to_dict): Failed decoding JSON '{s}'. Error: {e}")
108
+ except Exception as e:
109
+ print(f"Internal Error (_json_string_to_dict): {e}")
110
+ return res_dict
111
+ # --- End Internal Helpers ---
112
+
113
+ # 1. Convert input dictionary to JSON string
114
+ json_input_string = _dict_to_json_string(text_dict)
115
+ print(f"Input JSON String: {json_input_string}") # Debugging output
116
+ if json_input_string == "{}":
117
+ print("Skipping translation due to empty input dictionary or conversion error.")
118
+ return {key: "" for key in text_dict} # Return original structure with empty values
119
+
120
+
121
+ system_prompt = f"""Translate the string values within the following JSON object .
122
+
123
+ Follow these instructions carefully:
124
+ 1. Analyze the entire JSON object to understand the context.
125
+ 2. Translate *only* the string values.
126
+ 3. Keep the original keys *exactly* as they are.
127
+ 4. Do *not* translate non-string values (like hex color codes, numbers, or potentially proper nouns like 'CALISTOGA', 'DM SANS', 'Pexels', 'Pixabay' unless they have a common translation). Use your best judgment for proper nouns.
128
+ 5. Preserve the original JSON structure perfectly.
129
+ 6. Your output *must* be only the translated JSON object, without any introductory text, explanations, or markdown formatting like ```json ... ```.
130
+
131
+ """
132
+ # 3. Construct User Prompt
133
+ user_prompt = f"Source language: {source_lang}. Target language: {target_lang}. JSON String: {json_input_string} \n\n Translated JSON Output:"
134
+
135
+ # 4. Call the LLM API
136
+ raw_translated_json_string = "{}" # Default to empty JSON string
137
+ try:
138
+ model = genai.GenerativeModel('gemini-2.0-flash')
139
+ full_prompt = f"{system_prompt.strip()}\n\n{user_prompt.strip()}"
140
+
141
+ response = model.generate_content(
142
+ contents=full_prompt,
143
+ generation_config={
144
+ 'temperature': 0.3, # Low temp for adherence
145
+ 'top_p': 1,
146
+ 'top_k': 1,
147
+ }
148
+ # safety_settings=[...]
149
+ )
150
+
151
+ # Extract text safely and clean
152
+ if response and response.parts:
153
+ if hasattr(response.parts[0], 'text'):
154
+ raw_translated_json_string = response.parts[0].text.strip()
155
+ else:
156
+ print(f"Warning: Received response part without text attribute: {response.parts[0]}")
157
+ try: raw_translated_json_string = str(response.parts[0])
158
+ except Exception as str_e: print(f"Could not convert response part to string: {str_e}")
159
+ elif response and hasattr(response, 'text'):
160
+ raw_translated_json_string = response.text.strip()
161
+ else:
162
+ print(f"Warning: Received unexpected or empty response format from API: {response}")
163
+
164
+ # Clean potential markdown backticks
165
+ if raw_translated_json_string.startswith("```json"): raw_translated_json_string = raw_translated_json_string[7:]
166
+ if raw_translated_json_string.startswith("```"): raw_translated_json_string = raw_translated_json_string[3:]
167
+ if raw_translated_json_string.endswith("```"): raw_translated_json_string = raw_translated_json_string[:-3]
168
+ raw_translated_json_string = raw_translated_json_string.strip()
169
+ # Ensure it's at least plausible JSON before parsing
170
+ if not raw_translated_json_string: raw_translated_json_string = "{}"
171
+
172
+
173
+ except Exception as e:
174
+ print(f"Lỗi trong quá trình gọi API dịch: {e}")
175
+ raw_translated_json_string = "{}" # Ensure empty JSON on error
176
+
177
+ print(raw_translated_json_string)
178
+ # 5. Convert the LLM's JSON string response back to a dictionary
179
+ translated_intermediate_dict = _json_string_to_dict(raw_translated_json_string)
180
+
181
+ # 6. Validation: Ensure output dict has same keys as input dict
182
+ final_translated_dict = {}
183
+ missing_keys = []
184
+ for key in text_dict.keys(): # Iterate using ORIGINAL keys
185
+ if key in translated_intermediate_dict:
186
+ final_translated_dict[key] = translated_intermediate_dict[key]
187
+ else:
188
+ final_translated_dict[key] = "" # Preserve key, use empty string if missing
189
+ missing_keys.append(key)
190
+
191
+ if missing_keys:
192
+ print(f"Warning: LLM response was missing keys: {sorted(missing_keys)}. Filled with empty strings.")
193
+
194
+ extra_keys = set(translated_intermediate_dict.keys()) - set(text_dict.keys())
195
+ if extra_keys:
196
+ print(f"Warning: LLM response contained unexpected extra keys: {sorted(list(extra_keys))}. These were ignored.")
197
+
198
+
199
+ return final_translated_dict
200
+
201
+ # Function 3: Dictionary -> List
202
+ def postprocess_text(translated_dict):
203
+ """
204
+ Converts a dictionary {index: translated_text} back into a list of
205
+ strings, ordered by the index (key).
206
+ """
207
+ if not isinstance(translated_dict, dict):
208
+ print("Warning: postprocess_text expected a dict, received:", type(translated_dict))
209
+ return []
210
+ if not translated_dict:
211
+ return []
212
+
213
+ # Sort the dictionary items by key (index)
214
+ try:
215
+ # Ensure keys are integers for correct sorting if possible, handle errors
216
+ items_to_sort = []
217
+ for k, v in translated_dict.items():
218
+ try:
219
+ items_to_sort.append((int(k), v))
220
+ except (ValueError, TypeError):
221
+ print(f"Warning: postprocess cannot sort non-integer key '{k}', skipping.")
222
+ continue # Skip non-integer keys for sorting
223
+
224
+ if not items_to_sort:
225
+ print("Warning: No sortable items found in dictionary for postprocessing.")
226
+ return []
227
+
228
+ sorted_items = sorted(items_to_sort)
229
+
230
+ # Check for gaps in indices (optional but good practice)
231
+ expected_length = sorted_items[-1][0] + 1
232
+ if len(sorted_items) != expected_length:
233
+ print(f"Warning: Index gaps detected in postprocessing. Expected {expected_length} items based on max index, got {len(sorted_items)}.")
234
+ # Reconstruct carefully to handle gaps, filling with empty strings
235
+ result_list = [""] * expected_length
236
+ for index, text in sorted_items:
237
+ if 0 <= index < expected_length:
238
+ result_list[index] = text
239
+ return result_list
240
+
241
+ # If no gaps, simply extract values
242
+ translated_list = [text for index, text in sorted_items]
243
+ return translated_list
244
+
245
+ except Exception as e:
246
+ print(f"Error during postprocessing sorting/list creation: {e}")
247
+ return [] # Return empty list on error