Spaces:
Running
Running
update pptx
Browse files- db/__pycache__/mongodb.cpython-310.pyc +0 -0
- db/mongodb.py +1 -67
- home.py +2 -2
- pages/upload.py +12 -14
- powerpoint/__pycache__/pptx.cpython-310.pyc +0 -0
- powerpoint/__pycache__/xml_handling.cpython-310.pyc +0 -0
- powerpoint/pptx.py +139 -0
- powerpoint/xml_handling.py +513 -352
- test.ipynb +0 -0
- utils/__pycache__/utils.cpython-310.pyc +0 -0
- utils/utils.py +247 -0
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
|
| 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.
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 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
|
| 2 |
-
|
| 3 |
-
import
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 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 |
-
|
|
|
|
| 80 |
|
| 81 |
-
:
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
"""
|
| 85 |
-
#
|
| 86 |
-
|
| 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 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 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"
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 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 |
-
|
| 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 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
try:
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
else:
|
| 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 |
except Exception as e:
|
| 234 |
-
print(f"
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 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 |
-
|
|
|
|
| 270 |
"""
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
"""
|
| 277 |
-
#
|
| 278 |
-
|
| 279 |
-
|
| 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 |
-
|
| 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 |
except Exception as e:
|
| 373 |
-
print(f"
|
| 374 |
-
|
| 375 |
-
|
| 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
|