Spaces:
Running
Running
| """ | |
| HandwrittenOCR - الوحدة الرئيسية v6.0 Unified | |
| ======================================== | |
| نقطة الدخول الرئيسية - تجمع بين جميع المكونات. | |
| يدعم: التشغيل المحلي (Offline)، المزامنة بين الأجهزة، واجهة الشبكة المحلية. | |
| Gradio UI كاملة (7 تبويبات) + FP16 + memory_mode + HuggingFace Spaces. | |
| """ | |
| import time | |
| import logging | |
| import os | |
| from config import Config | |
| from src.logger import setup_logging | |
| from src.recognition import OCREngine | |
| from src.correction import init_correctors | |
| from src.database import HandwritingDB | |
| from src.pdf_processor import PDFProcessor | |
| from src.review_ui import ReviewUI | |
| from src.metrics import compute_metrics | |
| def _check_available_memory() -> tuple[int, int]: | |
| """كشف الذاكرة المتاحة (RAM + Swap) بالميجابايت.""" | |
| try: | |
| with open("/proc/meminfo", "r") as f: | |
| meminfo = {} | |
| for line in f: | |
| parts = line.split() | |
| if len(parts) >= 2: | |
| key = parts[0].rstrip(":") | |
| value = int(parts[1]) # كيلوبايت | |
| meminfo[key] = value | |
| mem_total = meminfo.get("MemTotal", 0) // 1024 # MB | |
| mem_available = meminfo.get("MemAvailable", 0) // 1024 # MB | |
| swap_total = meminfo.get("SwapTotal", 0) // 1024 # MB | |
| return mem_available, swap_total | |
| except Exception: | |
| return 0, 0 | |
| def _auto_detect_low_memory(config: Config) -> bool: | |
| """كشف تلقائي: هل يجب تفعيل الوضع الخفيف؟ | |
| يُفعّل تلقائياً إذا كانت الذاكرة المتاحة < 3 GB. | |
| يمكن تجاوزه يدوياً بـ config.low_memory. | |
| """ | |
| mem_available_mb, swap_mb = _check_available_memory() | |
| total_available = mem_available_mb + swap_mb | |
| logger = logging.getLogger("HandwrittenOCR") | |
| logger.info(f"الذاكرة المتاحة: {mem_available_mb} MB RAM + {swap_mb} MB Swap") | |
| # إذا الذاكرة < 3 GB: فعّل الوضع الخفيف تلقائياً | |
| if total_available < 3072 and not config.low_memory: | |
| logger.warning( | |
| f"الذاكرة المتاحة ({total_available} MB) أقل من 3 GB — " | |
| f"يُفعَّل الوضع الخفيف تلقائياً. استخدم --low-memory لتخطي هذا التحذير." | |
| ) | |
| config.low_memory = True | |
| return config.low_memory | |
| def main(config: Config | None = None): | |
| if config is None: | |
| config = Config() | |
| # إعداد شامل | |
| config.setup() | |
| config.apply_hf_token() | |
| config.apply_cache_env() | |
| if not config.is_colab: | |
| # لا حاجة لربط EasyOCR بالـ Drive محلياً | |
| pass | |
| else: | |
| config.setup_easyocr_symlink() | |
| logger = setup_logging(config) | |
| logger.info(f"بدء تشغيل HandwrittenOCR v6.0 Unified") | |
| logger.info(f"ملف PDF: {config.pdf_path}") | |
| logger.info(f"مجلد الإخراج: {config.output_dir or config.project_root}") | |
| if config.model_cache_dir or config.cache_dir: | |
| logger.info(f"تخزين مؤقت: {config.model_cache_dir or config.cache_dir}") | |
| # === كشف الذاكرة وتفعيل الوضع الخفيف تلقائياً === | |
| low_mem = _auto_detect_low_memory(config) | |
| if low_mem or config.low_memory: | |
| config.apply_low_memory() | |
| print("\n ⚠️ الوضع الخفيف مفعّل (توفير الذاكرة)") | |
| print(f" DPI={config.dpi}, EasyOCR={config.ocr_languages}, batch={config.trocr_batch_size}") | |
| print(" استخدم: python run.py --local --pdf FILE (بدون --low-memory) للوضع الكامل\n") | |
| # عرض معلومات المزامنة | |
| if config.sync_enabled: | |
| from src.sync import SyncManager | |
| sync_mgr = SyncManager(config) | |
| network = sync_mgr.get_network_info() | |
| print("\n" + "=" * 50) | |
| print(" نظام المزامنة: مفعّل") | |
| print("=" * 50) | |
| print(f" معرف الجهاز: {sync_mgr.device_id}") | |
| print(f" شبكة محلية: {network.get('local_ip', 'N/A')}") | |
| print(f" واجهة المراجعة: {network.get('server_url', 'N/A')}") | |
| print(f" API: {network.get('api_url', 'N/A')}") | |
| print("=" * 50) | |
| # كشف التعارضات | |
| conflicts = sync_mgr.detect_conflicts() | |
| if conflicts: | |
| for c in conflicts: | |
| print(f" تحذير: {c['message']}") | |
| # تحميل المدققات الإملائية (اختياري — يمكن تخطيه في الوضع الخفيف) | |
| if not config.skip_spellcheck: | |
| init_correctors() | |
| else: | |
| logger.info("تخطي المدققات الإملائية (توفير الذاكرة)") | |
| # تحميل محرك التعرف (مع LoRA تلقائي) | |
| start = time.time() | |
| ocr_engine = OCREngine( | |
| trocr_model_name=config.trocr_model_name, | |
| ocr_languages=config.ocr_languages, | |
| max_text_length=config.max_text_length, | |
| cache_dir=config.model_cache_dir or config.cache_dir, | |
| hf_token=config.hf_token, | |
| trocr_default_confidence=config.trocr_default_confidence, | |
| easy_conf_threshold=config.easy_conf_threshold, | |
| num_beams=config.num_beams, | |
| trocr_batch_size=config.trocr_batch_size, | |
| lora_save_path=config.lora_save_path, | |
| skip_trocr=config.skip_trocr, | |
| ) | |
| logger.info(f"تم تحميل النماذج في {time.time() - start:.2f} ثانية") | |
| if ocr_engine.lora_loaded: | |
| print("تم تحميل النموذج المُحسَّن (LoRA)") | |
| else: | |
| print("يستخدم النموذج الأساسي") | |
| # تهيئة قاعدة البيانات | |
| db = HandwritingDB(config.db_path) | |
| # معالجة PDF | |
| processor = PDFProcessor(config, ocr_engine, db) | |
| stats = processor.process() | |
| if stats.get("error"): | |
| logger.error("فشلت المعالجة!") | |
| if isinstance(stats.get("error"), str): | |
| logger.error(f"السبب: {stats['error']}") | |
| if stats.get("error") == "lock_timeout": | |
| print("\nتعذر الحصول على قفل المعالجة - جهاز آخر يعمل حالياً.") | |
| print("انتظر حتى يكتمل الجهاز الآخر أو أوقف المعالجة عليه.") | |
| return | |
| # عرض الإحصائيات | |
| print("\n" + "=" * 50) | |
| print(" إحصائيات المعالجة v6.0 Unified") | |
| print("=" * 50) | |
| print(f" Run ID: {stats.get('run_id', 'N/A')}") | |
| print(f" الصفحات: {stats.get('pages', 0)}") | |
| print(f" الكلمات: {stats.get('words', 0)}") | |
| print(f" متوسط الثقة: {stats.get('avg_confidence', 0):.2%}") | |
| print(f" الوقت: {stats.get('duration_sec', 0):.1f} ثانية") | |
| print("=" * 50) | |
| # حساب المقاييس (WER/CER) | |
| if config.metrics_log: | |
| try: | |
| m = compute_metrics(db, metrics_log=config.metrics_log) | |
| if m.get("wer") is not None: | |
| print(f"\n WER: {m['wer']:.2%} | CER: {m['cer']:.2%} ({m['samples']} عينة)") | |
| except Exception as e: | |
| logger.debug(f"Metrics: {e}") | |
| # ملفات المراقبة | |
| print(f"\nملفات المراقبة:") | |
| print(f" سجل الأحداث: {config.log_file}") | |
| print(f" إحصائيات: {config.stats_json}") | |
| print(f" تصحيحات: {config.feedback_csv}") | |
| print(f" قاموس التصحيح: {config.correction_dict_path}") | |
| if config.sync_enabled: | |
| print(f" حالة المزامنة: {config.sync_status_path}") | |
| # تشغيل واجهة المراجعة | |
| print("\nتشغيل واجهة المراجعة...") | |
| review_ui = ReviewUI(db, config.feedback_csv) | |
| review_ui.launch() | |
| # تشغيل Gradio UI كاملة (7 تبويبات) في بيئة Colab أو عند تفعيل المشاركة | |
| if config.is_colab or config.gradio_share: | |
| from src.gradio_ui import launch_gradio | |
| launch_gradio(config) | |
| if __name__ == "__main__": | |
| main() | |