Dr. Abdulmalek
deploy: OmniFile AI Processor v4.3.0
900df0b
"""
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()