""" File text extractor for the L0 quality layer. Extracts a small sample of text from a translated file so the L0 checks can run on a real output without requiring the translators to expose their internal chunk data. This module depends on the same libraries the translators use (python-docx, openpyxl, python-pptx, PyMuPDF). All imports are lazy and guarded, so a missing library only blocks the matching format — other formats keep working. """ from __future__ import annotations import zipfile from pathlib import Path from typing import List, Optional, TypedDict class TextSample(TypedDict): """A (source_placeholder, translated) pair from an output file.""" source: str translated: str # Maximum samples per format — keeps the L0 check fast. DEFAULT_MAX_SAMPLES = 20 def extract_sample( file_path: Path, file_extension: str, max_samples: int = DEFAULT_MAX_SAMPLES, ) -> List[TextSample]: """ Extract a sample of translated text strings from a finished file. The "source" field is always empty — we don't have the original document at this point. The L0 checks that care about source/target ratio (length_checker) handle empty source gracefully. Returns an empty list if the file cannot be read. """ if not file_path or not Path(file_path).exists(): return [] ext = (file_extension or Path(file_path).suffix).lower() try: if ext == ".docx": return _extract_docx(file_path, max_samples) if ext == ".xlsx": return _extract_xlsx(file_path, max_samples) if ext == ".pptx": return _extract_pptx(file_path, max_samples) if ext == ".pdf": return _extract_pdf(file_path, max_samples) except Exception: # Any failure: return an empty list. The route will log it. return [] return [] def _extract_docx(path: Path, max_samples: int) -> List[TextSample]: """Extract text from a Word document.""" from docx import Document doc = Document(str(path)) samples: List[TextSample] = [] for para in doc.paragraphs: text = (para.text or "").strip() if text and len(text) > 5: samples.append({"source": "", "translated": text}) if len(samples) >= max_samples: break # If body had nothing, try tables. if not samples: for table in doc.tables: for row in table.rows: for cell in row.cells: text = (cell.text or "").strip() if text and len(text) > 5: samples.append({"source": "", "translated": text}) if len(samples) >= max_samples: return samples return samples def _extract_xlsx(path: Path, max_samples: int) -> List[TextSample]: """Extract text from an Excel file.""" from openpyxl import load_workbook wb = load_workbook(str(path), data_only=True, read_only=True) samples: List[TextSample] = [] try: for ws in wb.worksheets: for row in ws.iter_rows(): for cell in row: val = cell.value if isinstance(val, str): text = val.strip() if text and len(text) > 3: samples.append({"source": "", "translated": text}) if len(samples) >= max_samples: return samples finally: wb.close() return samples def _extract_pptx(path: Path, max_samples: int) -> List[TextSample]: """Extract text from a PowerPoint file.""" from pptx import Presentation pres = Presentation(str(path)) samples: List[TextSample] = [] for slide in pres.slides: for shape in slide.shapes: if not shape.has_text_frame: continue for para in shape.text_frame.paragraphs: text = (para.text or "").strip() if text and len(text) > 3: samples.append({"source": "", "translated": text}) if len(samples) >= max_samples: return samples return samples def _extract_pdf(path: Path, max_samples: int) -> List[TextSample]: """Extract text from a PDF file (best-effort, layout-preserving format).""" try: import fitz # PyMuPDF except ImportError: return [] samples: List[TextSample] = [] doc = fitz.open(str(path)) try: for page in doc: text = page.get_text("text") or "" for line in text.splitlines(): line = line.strip() if line and len(line) > 5: samples.append({"source": "", "translated": line}) if len(samples) >= max_samples: return samples finally: doc.close() return samples