File size: 11,551 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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
#!/usr/bin/env python3
"""
سكربت شامل: OCR -> تصحيح مختلط -> كشف جداول -> تطبيع -> تصدير مطابق.
========================================================================
سير عمل متكامل من الصورة إلى التصدير المنسق.

الاستخدام:
    # معالجة كاملة مع التصحيح والتصدير
    python process.py --input image.jpg --output out --engine surya \\
        --langs ar en --correct --detect-tables --export-docx

    # OCR فقط مع Surya
    python process.py --input image.jpg --output out --engine surya

    # مع Tesseract والتصدير
    python process.py --input image.jpg --output out --engine tesseract \\
        --correct --export-docx

المؤلف: Dr Abdulmalek Tamer Al-husseini
الترخيص: MIT
"""

import argparse
import json
import os
import sys
import logging
from datetime import datetime

# إضافة جذر المشروع إلى المسار
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger("process")


def parse_args():
    """تحليل معلمات سطر الأوامر."""
    parser = argparse.ArgumentParser(
        description="سكربت معالجة متكامل لـ OmniFile Processor",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
أمثلة:
  python process.py -i image.jpg -o results -e surya
  python process.py -i doc.pdf -o out -e tesseract --correct --export-docx
  python process.py -i scan.png -o out -e surya --detect-tables --langs ar en
        """,
    )
    parser.add_argument(
        "-i", "--input", required=True,
        help="مسار ملف الإدخال (صورة JPG/PNG أو PDF)",
    )
    parser.add_argument(
        "-o", "--output", required=True,
        help="مسار مجلد الإخراج",
    )
    parser.add_argument(
        "-e", "--engine", default="tesseract",
        choices=["tesseract", "easyocr", "trocr", "paddleocr", "surya"],
        help="محرك OCR (الافتراضي: tesseract)",
    )
    parser.add_argument(
        "-l", "--langs", nargs="+", default=["ar", "en"],
        help="اللغات المطلوبة (الافتراضي: ar en)",
    )
    parser.add_argument(
        "--correct", action="store_true",
        help="تفعيل تصحيح اللغات المختلطة",
    )
    parser.add_argument(
        "--detect-tables", action="store_true",
        help="تفعيل كشف الجداول (يتطلب transformers+torch)",
    )
    parser.add_argument(
        "--export-docx", action="store_true",
        help="تصدير DOCX مطابق للتنسيق",
    )
    parser.add_argument(
        "--threshold", type=float, default=0.7,
        help="عتبة كشف الجداول (الافتراضي: 0.7)",
    )
    parser.add_argument(
        "--verbose", "-v", action="store_true",
        help="عرض معلومات تفصيلية",
    )
    return parser.parse_args()


def run_ocr_by_engine(engine_name: str, image_path: str, langs: list[str]):
    """
    تشغيل محرك OCR المحدد وإرجاع (نص, كتل خام).

    Args:
        engine_name: اسم المحرك
        image_path: مسار الصورة
        langs: اللغات المطلوبة

    Returns:
        tuple (نص كامل, قائمة كتل)
    """
    if engine_name == "surya":
        from modules.vision.surya_ocr import SuryaOCREngine
        engine = SuryaOCREngine(langs=langs)
        text, blocks = engine.extract_text(image_path)
        return text, blocks

    elif engine_name == "tesseract":
        import pytesseract
        from PIL import Image
        image = Image.open(image_path)
        text = pytesseract.image_to_string(image, lang="+".join(langs))
        blocks = []
        for line in text.strip().split("\n"):
            if line.strip():
                blocks.append({
                    "type": "paragraph",
                    "bbox": [0, 0, 1, 1],
                    "text": line.strip(),
                    "confidence": 0.8,
                })
        return text, blocks

    elif engine_name == "easyocr":
        import easyocr
        import numpy as np
        from PIL import Image
        reader = easyocr.Reader(langs, gpu=False, verbose=False)
        img = np.array(Image.open(image_path).convert("RGB"))
        results = reader.readtext(img)
        blocks = []
        full_text = []
        for bbox, text, conf in results:
            blocks.append({
                "type": "paragraph",
                "bbox": [0, 0, 1, 1],
                "text": text,
                "confidence": float(conf),
            })
            full_text.append(text)
        return "\n".join(full_text), blocks

    elif engine_name == "trocr":
        from transformers import TrOCRProcessor, VisionEncoderDecoderModel
        from PIL import Image
        import torch
        processor = TrOCRProcessor.from_pretrained(
            "microsoft/trocr-base-handwritten"
        )
        model = VisionEncoderDecoderModel.from_pretrained(
            "microsoft/trocr-base-handwritten"
        )
        image = Image.open(image_path).convert("RGB")
        pixel_values = processor(image, return_tensors="pt").pixel_values
        with torch.no_grad():
            generated_ids = model.generate(pixel_values)
        text = processor.batch_decode(
            generated_ids, skip_special_tokens=True
        )[0]
        blocks = [{
            "type": "paragraph",
            "bbox": [0, 0, 1, 1],
            "text": text,
            "confidence": 0.85,
        }]
        return text, blocks

    elif engine_name == "paddleocr":
        from paddleocr import PaddleOCR
        import numpy as np
        from PIL import Image
        ocr = PaddleOCR(use_angle_cls=True, lang="ar", use_gpu=False, show_log=False)
        img = np.array(Image.open(image_path).convert("RGB"))
        results = ocr.ocr(img, cls=True)
        blocks = []
        full_text = []
        if results and results[0]:
            for item in results[0]:
                text, conf = item[1]
                blocks.append({
                    "type": "paragraph",
                    "bbox": [0, 0, 1, 1],
                    "text": text,
                    "confidence": float(conf),
                })
                full_text.append(text)
        return "\n".join(full_text), blocks

    else:
        raise ValueError(f"محرك غير معروف: {engine_name}")


def main():
    """الدالة الرئيسية لسير العمل المتكامل."""
    args = parse_args()

    if args.verbose:
        logging.getLogger().setLevel(logging.DEBUG)

    start_time = datetime.now()
    logger.info("=" * 60)
    logger.info("OmniFile Processor — سير العمل المتكامل")
    logger.info("=" * 60)
    logger.info("ملف الإدخال:  %s", args.input)
    logger.info("مجلد الإخراج: %s", args.output)
    logger.info("المحرك:        %s", args.engine)
    logger.info("اللغات:         %s", args.langs)
    logger.info("-" * 60)

    # إنشاء مجلد الإخراج
    os.makedirs(args.output, exist_ok=True)

    # ─── 1. تشغيل OCR ───
    logger.info("[1/5] تشغيل OCR بمحرك %s...", args.engine)
    try:
        text, raw_blocks = run_ocr_by_engine(
            args.engine, args.input, args.langs
        )
        logger.info(
            "تم استخراج %d حرف في %d كتلة",
            len(text),
            len(raw_blocks),
        )
    except Exception as e:
        logger.error("فشل OCR: %s", e)
        sys.exit(1)

    # ─── 2. تصحيح النص المختلط ───
    if args.correct:
        logger.info("[2/5] تصحيح اللغات المختلطة...")
        try:
            from modules.nlp.mixed_language import MixedLanguageHandler
            handler = MixedLanguageHandler(languages=args.langs)
            text = handler.correct_text_mixed(text)
            for block in raw_blocks:
                if "text" in block:
                    block["text"] = handler.correct_text_mixed(block["text"])
            logger.info("تم تصحيح النص المختلط")
        except Exception as e:
            logger.warning("فشل التصحيح المختلط: %s", e)
    else:
        logger.info("[2/5] تصحيح مختلط: معطّل")

    # ─── 3. كشف الجداول ───
    if args.detect_tables:
        logger.info("[3/5] كشف الجداول (TATR)...")
        try:
            from modules.vision.table_detection import TableDetectionTransformer
            detector = TableDetectionTransformer(device="cpu")
            tables = detector.detect_tables(args.input, threshold=args.threshold)
            logger.info("تم كشف %d جدول", len(tables))

            # تحويل الجداول المكتشفة إلى كتل وإضافتها
            from PIL import Image
            image = Image.open(args.input)
            w, h = image.size
            for t in tables:
                x1, y1, x2, y2 = t["bbox"]
                bbox_rel = [x1 / w, y1 / h, x2 / w, y2 / h]
                raw_blocks.append({
                    "type": "table",
                    "bbox": bbox_rel,
                    "confidence": t["score"],
                    "cells": [],
                    "label": t["label"],
                })
        except Exception as e:
            logger.warning("فشل كشف الجداول: %s", e)
    else:
        logger.info("[3/5] كشف الجداول: معطّل")

    # ─── 4. تطبيع النتائج ───
    logger.info("[4/5] تطبيع النتائج إلى الهيكل القياسي...")
    try:
        from modules.vision.normalize import (
            normalize_ocr_output,
            save_normalized,
        )
        from PIL import Image

        img = Image.open(args.input)
        normalized = normalize_ocr_output(
            raw_blocks, args.input, img.width, img.height,
            args.engine, args.langs,
        )
        json_path = os.path.join(args.output, "result.json")
        save_normalized(normalized, json_path)
        logger.info("تم حفظ النتيجة الموحدة: %s", json_path)
    except Exception as e:
        logger.error("فشل التطبيع: %s", e)
        sys.exit(1)

    # ─── 5. تصدير DOCX ───
    if args.export_docx:
        logger.info("[5/5] تصدير DOCX مطابق للتنسيق...")
        try:
            from modules.export.layout_preserving import layout_to_docx
            docx_path = os.path.join(args.output, "output_preserved.docx")
            layout_to_docx(json_path, docx_path)
            logger.info("تم تصدير الملف المنسق: %s", docx_path)
        except Exception as e:
            logger.warning("فشل تصدير DOCX: %s", e)
    else:
        logger.info("[5/5] تصدير DOCX: معطّل")

    # ─── ملخص ───
    elapsed = (datetime.now() - start_time).total_seconds()
    logger.info("-" * 60)
    logger.info("تمت المعالجة بنجاح!")
    logger.info("  الوقت: %.2f ثانية", elapsed)
    logger.info("  الكتل: %d", len(raw_blocks))
    logger.info("  المحرك: %s", args.engine)
    logger.info("  الإخراج: %s", args.output)
    logger.info("=" * 60)


if __name__ == "__main__":
    main()