import re import os import time import logging from pathlib import Path from typing import Tuple logger = logging.getLogger(__name__) def reshape_arabic_text(text: str) -> str: """Reshape Arabic text for proper display.""" try: import arabic_reshaper from bidi.algorithm import get_display reshaped = arabic_reshaper.reshape(text) bidi_text = get_display(reshaped) return bidi_text except Exception: return text def validate_file(file_path: str, max_size_mb: int = 20) -> Tuple[bool, str]: """Validate uploaded file size and extension.""" from config import SUPPORTED_FORMATS path = Path(file_path) if not path.exists(): return False, "File does not exist" ext = path.suffix.lower() if ext not in SUPPORTED_FORMATS: return False, f"Unsupported format: {ext}. Supported: {', '.join(SUPPORTED_FORMATS)}" size_mb = path.stat().st_size / (1024 * 1024) if size_mb > max_size_mb: return False, f"File too large: {size_mb:.1f}MB. Maximum: {max_size_mb}MB" return True, "OK" def format_file_size(size_bytes: int) -> str: """Human-readable file size.""" if size_bytes < 1024: return f"{size_bytes} B" elif size_bytes < 1024 * 1024: return f"{size_bytes / 1024:.1f} KB" else: return f"{size_bytes / (1024 * 1024):.1f} MB" def truncate_text(text: str, max_chars: int = 500) -> str: """Truncate long text for display.""" if len(text) <= max_chars: return text return text[:max_chars] + "..." def calculate_arabic_ratio(text: str) -> float: """Calculate ratio of Arabic characters in text.""" if not text: return 0.0 arabic_chars = len(re.findall(r'[\u0600-\u06FF]', text)) return round(arabic_chars / len(text), 4) def timer_ms(func): """Decorator to measure function execution time in ms.""" def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) elapsed = (time.time() - start) * 1000 logger.info(f"{func.__name__} took {elapsed:.2f}ms") return result, elapsed return wrapper def clean_arabic_text(text: str) -> str: """Basic Arabic text cleaning.""" # Remove non-printable characters text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]', '', text) # Normalize whitespace text = re.sub(r'\s+', ' ', text).strip() # Remove standalone punctuation clusters text = re.sub(r'([^\w\s])\1{3,}', r'\1', text) return text def build_summary_display(structured_doc: dict) -> str: """Build a human-readable summary of structured document.""" if not structured_doc: return "No document processed yet." meta = structured_doc.get("metadata", {}) analysis = structured_doc.get("document_analysis", {}) entities = structured_doc.get("extracted_entities", {}) dates = structured_doc.get("extracted_dates", []) financials = structured_doc.get("financial_data", {}) lines = [ f"📄 **Document ID:** {structured_doc.get('document_id', 'N/A')}", f"📁 **Filename:** {meta.get('filename', 'N/A')}", f"🔍 **Type:** {analysis.get('document_type', 'N/A')}", f"🌐 **Language:** {analysis.get('language', 'N/A')}", f"📊 **Pages:** {analysis.get('total_pages', 0)} | **Words:** {analysis.get('total_words', 0)}", f"⚙️ **Extraction:** {meta.get('extraction_source', 'N/A')} | **Confidence:** {meta.get('extraction_confidence', 0):.2%}", "", f"📅 **Dates Found:** {', '.join(dates) if dates else 'None'}", f"💰 **Amounts:** {', '.join(financials.get('amounts', [])) if financials.get('amounts') else 'None'}", f"📧 **Emails:** {', '.join(entities.get('emails', [])) if entities.get('emails') else 'None'}", f"📞 **Phones:** {', '.join(entities.get('phone_numbers', [])) if entities.get('phone_numbers') else 'None'}", ] return "\n".join(lines)