import os import torch from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, M2M100ForConditionalGeneration, M2M100Tokenizer from pypdf import PdfReader import docx LANGUAGES = { "Tự động phát hiện": "auto", "Tiếng Việt": "vi", "Tiếng Anh": "en", "Tiếng Nhật": "ja", "Tiếng Hàn": "ko", "Tiếng Trung (Giản thể)": "zh-CN", "Tiếng Trung (Phồn thể)": "zh-TW", "Tiếng Pháp": "fr", "Tiếng Đức": "de", "Tiếng Tây Ban Nha": "es", "Tiếng Nga": "ru" } STYLES = { "Mặc định": "Dịch chính xác, tự nhiên.", "Trang trọng, lịch sự": "Dịch theo văn phong trang trọng, lịch sự, sử dụng từ ngữ kính trọng (ví dụ: dùng 'quý khách', 'chúng tôi', xưng hô lịch sự, từ ngữ trang nhã).", "Thân mật, gần gũi": "Dịch theo văn phong thân mật, gần gũi, tự nhiên như giao tiếp hàng ngày (ví dụ: dùng 'bạn', 'mình', 'tớ', câu văn tự nhiên, thoải mái).", "Học thuật, chuyên ngành": "Dịch theo phong cách học thuật, khoa học chuyên ngành, sử dụng chính xác các thuật ngữ chuyên môn.", "Văn học, nghệ thuật": "Dịch theo phong cách văn thơ bay bổng, uyển chuyển, giàu tính nhạc và hình ảnh nghệ thuật." } # Cache for loaded local models _LOCAL_MODEL_CACHE = {} def get_local_model_and_tokenizer(model_name: str): """Load and cache local translation model from Hugging Face.""" global _LOCAL_MODEL_CACHE if model_name in _LOCAL_MODEL_CACHE: return _LOCAL_MODEL_CACHE[model_name] print(f"📦 Loading local translation model: {model_name}...") if "m2m100" in model_name: tokenizer = M2M100Tokenizer.from_pretrained(model_name) model = M2M100ForConditionalGeneration.from_pretrained(model_name) else: try: if "opus-mt" in model_name or "helsinki" in model_name.lower(): from transformers import MarianTokenizer tokenizer = MarianTokenizer.from_pretrained(model_name) else: tokenizer = AutoTokenizer.from_pretrained(model_name) except Exception as e: print(f"⚠️ Cảnh báo: AutoTokenizer thất bại, chuyển sang MarianTokenizer cho {model_name}: {e}") try: from transformers import MarianTokenizer tokenizer = MarianTokenizer.from_pretrained(model_name) except Exception: tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForSeq2SeqLM.from_pretrained(model_name) device = "cuda" if torch.cuda.is_available() else "cpu" model = model.to(device) _LOCAL_MODEL_CACHE[model_name] = (model, tokenizer, device) return model, tokenizer, device def apply_vietnamese_style(text: str, style: str) -> str: """Adjust pronouns and word choices in Vietnamese text to match the requested style.""" if not text or style == "Mặc định": return text if style == "Trang trọng, lịch sự": replacements = { " tôi ": " chúng tôi ", "Tôi ": "Chúng tôi ", " tớ ": " chúng tôi ", "Tớ ": "Chúng tôi ", " mình ": " chúng tôi ", "Mình ": "Chúng tôi ", " cậu ": " quý khách ", "Cậu ": "Quý khách ", " mày ": " quý khách ", "Mày ": "Quý khách ", " bạn ": " quý khách ", "Bạn ": "Quý khách ", } for k, v in replacements.items(): text = text.replace(k, v) elif style == "Thân mật, gần gũi": replacements = { " tôi ": " mình ", "Tôi ": "Mình ", " chúng tôi ": " chúng mình ", "Chúng tôi ": "Chúng mình ", " quý khách ": " bạn ", "Quý khách ": "Bạn ", " ngài ": " bạn ", "Ngài ": "Bạn ", } for k, v in replacements.items(): text = text.replace(k, v) elif style == "Văn học, nghệ thuật": replacements = { " nhanh chóng ": " mau chóng ", " rất ": " vô cùng ", " đẹp ": " thơ mộng ", " nói ": " bộc bạch ", } for k, v in replacements.items(): text = text.replace(k, v) return text def translate_text(text: str, target_lang: str, source_lang: str = "auto", style: str = "Mặc định") -> str: """Translate plain text locally in small chunks of 500 characters.""" if not text or not text.strip(): return "" src = LANGUAGES.get(source_lang, "auto") tgt = LANGUAGES.get(target_lang, "vi") if src == "auto": src = "en" # Fallback auto to English source # Determine model name if src == "en" and tgt == "vi": model_name = "Helsinki-NLP/opus-mt-en-vi" elif src == "vi" and tgt == "en": model_name = "Helsinki-NLP/opus-mt-vi-en" elif src == "zh-CN" and tgt == "vi": model_name = "Helsinki-NLP/opus-mt-zh-vi" elif src == "fr" and tgt == "vi": model_name = "Helsinki-NLP/opus-mt-fr-vi" elif src == "de" and tgt == "vi": model_name = "Helsinki-NLP/opus-mt-de-vi" else: model_name = "facebook/m2m100_418M" model, tokenizer, device = get_local_model_and_tokenizer(model_name) # Split text into sentences or small chunks (approx 500 characters) to avoid generation cutoff chunk_size = 500 paragraphs = text.split("\n") translated_paragraphs = [] for para in paragraphs: if not para.strip(): translated_paragraphs.append("") continue # Split paragraph into chunks of 500 chars chunks = [para[i:i+chunk_size] for i in range(0, len(para), chunk_size)] translated_chunks = [] for chunk in chunks: if not chunk.strip(): translated_chunks.append(chunk) continue try: if "m2m100" in model_name: src_m2m = src.split("-")[0] tgt_m2m = tgt.split("-")[0] tokenizer.src_lang = src_m2m inputs = tokenizer(chunk, return_tensors="pt").to(device) with torch.no_grad(): generated_tokens = model.generate(**inputs, forced_bos_token_id=tokenizer.get_lang_id(tgt_m2m), max_length=512) translated = tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)[0] else: inputs = tokenizer(chunk, return_tensors="pt", padding=True, truncation=True, max_length=512).to(device) with torch.no_grad(): outputs = model.generate(**inputs, max_length=512) translated = tokenizer.decode(outputs[0], skip_special_tokens=True) # Apply Vietnamese style styling if target is Vietnamese if tgt == "vi": translated = apply_vietnamese_style(translated, style) translated_chunks.append(translated) except Exception as e: print(f"Error local translating chunk: {e}") translated_chunks.append(chunk) translated_paragraphs.append("".join(translated_chunks)) return "\n".join(translated_paragraphs) def translate_docx(file_path: str, target_lang: str, source_lang: str = "auto", style: str = "Mặc định") -> str: """Translate a Word (.docx) document locally paragraph by paragraph.""" doc = docx.Document(file_path) for i, p in enumerate(doc.paragraphs): if p.text.strip(): try: p.text = translate_text(p.text, target_lang, source_lang, style) except Exception as e: print(f"Error translating docx paragraph {i}: {e}") for t in doc.tables: for row in t.rows: for cell in row.cells: for p in cell.paragraphs: if p.text.strip(): try: p.text = translate_text(p.text, target_lang, source_lang, style) except Exception as e: print(f"Error translating table paragraph: {e}") dir_name = os.path.dirname(file_path) base_name = os.path.basename(file_path) out_name = f"translated_{base_name}" out_path = os.path.join(dir_name, out_name) doc.save(out_path) return out_path def translate_pdf(file_path: str, target_lang: str, source_lang: str = "auto", style: str = "Mặc định") -> tuple[str, str]: """Extract text from PDF, translate locally, and save it as a new .docx file.""" reader = PdfReader(file_path) text_content = [] for page in reader.pages: page_text = page.extract_text() if page_text: text_content.append(page_text) full_text = "\n\n".join(text_content) translated_text = translate_text(full_text, target_lang, source_lang, style) dir_name = os.path.dirname(file_path) base_name = os.path.basename(file_path) out_docx_name = f"translated_{os.path.splitext(base_name)[0]}.docx" out_docx_path = os.path.join(dir_name, out_docx_name) doc = docx.Document() for paragraph in translated_text.split("\n\n"): if paragraph.strip(): doc.add_paragraph(paragraph) doc.save(out_docx_path) return out_docx_path, translated_text def translate_document(file_path: str, target_lang: str, source_lang: str = "auto", style: str = "Mặc định") -> tuple[str, str]: """Main function to parse extension and translate.""" if not file_path or not os.path.exists(file_path): return None, "❌ File không tồn tại." ext = os.path.splitext(file_path)[1].lower() base_name = os.path.basename(file_path) dir_name = os.path.dirname(file_path) try: if ext == ".docx": out_path = translate_docx(file_path, target_lang, source_lang, style) doc = docx.Document(out_path) preview_text = "\n".join([p.text for p in doc.paragraphs[:10]]) return out_path, preview_text elif ext == ".pdf": out_path, translated_text = translate_pdf(file_path, target_lang, source_lang, style) return out_path, translated_text[:1000] elif ext in [".txt", ".md", ".json"]: with open(file_path, "r", encoding="utf-8", errors="ignore") as f: content = f.read() translated_text = translate_text(content, target_lang, source_lang, style) out_name = f"translated_{base_name}" out_path = os.path.join(dir_name, out_name) with open(out_path, "w", encoding="utf-8") as f: f.write(translated_text) return out_path, translated_text[:1000] else: return None, f"❌ Định dạng file {ext} không được hỗ trợ." except Exception as e: import traceback traceback.print_exc() return None, f"❌ Lỗi trong quá trình dịch: {str(e)}"