File size: 8,244 Bytes
900df0b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
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
"""
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()