File size: 5,874 Bytes
4dffb27
0e8b849
 
 
 
4dffb27
 
9ecde69
8c4a20f
 
95c55af
9ecde69
8c4a20f
744219a
 
95c55af
4dffb27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c4a20f
4dffb27
744219a
8c4a20f
 
0e8b849
8c4a20f
744219a
9ecde69
 
744219a
4dffb27
 
 
 
 
 
8c4a20f
4dffb27
 
 
9ecde69
4dffb27
8c4a20f
 
0e8b849
 
8c4a20f
0e8b849
 
 
744219a
0e8b849
 
 
 
 
 
 
 
9ecde69
4dffb27
8c4a20f
744219a
 
 
8c4a20f
 
 
 
 
 
 
 
 
744219a
4dffb27
744219a
 
4dffb27
744219a
 
 
4dffb27
744219a
 
8c4a20f
744219a
8c4a20f
c8b8e71
4dffb27
8c4a20f
744219a
0e8b849
 
 
 
744219a
8649d54
8c4a20f
 
 
0e8b849
8c4a20f
 
 
 
 
0e8b849
c8b8e71
 
 
9ecde69
4dffb27
8c4a20f
 
 
0e8b849
 
9ecde69
8649d54
8c4a20f
0e8b849
 
 
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
import os, io, json, traceback
import gradio as gr
import fitz  # PyMuPDF
from PIL import Image

import spaces  # مطلوب ل ZeroGPU
from huggingface_hub import hf_hub_download

# ===== إعدادات النموذج =====
HOROOF_MODEL_NAME = os.environ.get("HOROOF_MODEL", "NaserNajeh/Horoof")

# تحميل كسول لتقليل زمن الإقلاع
_model = None
_tokenizer = None
_image_processor = None

def _load_clean_config(repo_id: str):
    """تحميل config.json وإزالة أي quantization_config لتفادي bitsandbytes."""
    from transformers import AutoConfig
    try:
        cfg_path = hf_hub_download(repo_id=repo_id, filename="config.json")
        with open(cfg_path, "r", encoding="utf-8") as f:
            cfg_json = json.load(f)
        # أزل أي مفاتيح قد تفرض bnb
        cfg_json.pop("quantization_config", None)
        cfg_json.pop("load_in_4bit", None)
        cfg_json.pop("load_in_8bit", None)
        return AutoConfig.from_dict(cfg_json)
    except Exception:
        # احتياطي: لو فشل، خذ الإعدادات الافتراضية
        return AutoConfig.from_pretrained(repo_id)

def load_horoof():
    """تحميل نموذج Horoof (Qwen2-VL) على الـGPU عند أول استدعاء فقط، بدون bitsandbytes/torchvision."""
    global _model, _tokenizer, _image_processor
    if _model is not None:
        return
    try:
        import torch
        from transformers import Qwen2VLForConditionalGeneration, AutoTokenizer, AutoImageProcessor
        if not torch.cuda.is_available():
            raise AssertionError("هذه النسخة تتطلب GPU (CUDA) مفعّل على الـSpace.")

        # إعدادات خفيفة: نمنع أي كمّية كي لا يُستدعى bitsandbytes
        cfg = _load_clean_config(HOROOF_MODEL_NAME)

        _tokenizer = AutoTokenizer.from_pretrained(HOROOF_MODEL_NAME, trust_remote_code=False)
        _image_processor = AutoImageProcessor.from_pretrained(HOROOF_MODEL_NAME, trust_remote_code=False)

        _model = Qwen2VLForConditionalGeneration.from_pretrained(
            HOROOF_MODEL_NAME,
            config=cfg,
            torch_dtype=torch.float16,   # fp16 على الـGPU
        ).to("cuda")

    except Exception as e:
        raise RuntimeError(f"تعذّر تحميل النموذج: {e}")

def pdf_to_images(pdf_bytes: bytes, dpi: int = 220, max_pages: int = 0):
    """تحويل PDF إلى قائمة صور PIL."""
    pages_imgs = []
    doc = fitz.open(stream=pdf_bytes, filetype="pdf")
    total = doc.page_count
    n_pages = total if (not max_pages or max_pages <= 0) else min(max_pages, total)
    for i in range(n_pages):
        page = doc.load_page(i)
        pix = page.get_pixmap(dpi=dpi, alpha=False)
        img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
        pages_imgs.append((i + 1, img))
    doc.close()
    return pages_imgs

def ocr_page_with_horoof(pil_img: Image.Image, max_new_tokens: int = 1200) -> str:
    """تشغيل Horoof على صورة صفحة واحدة (بدون quantization)."""
    load_horoof()
    import torch

    # رسالة محادثة متوافقة مع Qwen2-VL
    messages = [
        {
            "role": "user",
            "content": [
                {"type": "image", "image": pil_img},
                {"type": "text", "text": "اقرأ النص العربي في الصورة كما هو دون أي تعديل أو تفسير."},
            ],
        }
    ]

    # قالب المحادثة كنص (بدون تقطيع) ثم نقاطّع
    prompt = _tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)

    # مدخلات الصورة + النص
    vision_inputs = _image_processor(images=pil_img, return_tensors="pt")
    text_inputs = _tokenizer([prompt], return_tensors="pt")
    inputs = {**vision_inputs, **text_inputs}
    inputs = {k: (v.to("cuda") if hasattr(v, "to") else v) for k, v in inputs.items()}

    # توليد
    output_ids = _model.generate(**inputs, max_new_tokens=max_new_tokens)
    text = _tokenizer.batch_decode(output_ids, skip_special_tokens=True)[0]
    return (text or "").strip()

@spaces.GPU  # ضروري ل ZeroGPU: يجعل الاستدعاء يحجز GPU
def ocr_pdf(pdf_file, dpi, limit_pages):
    """الدالة الرئيسة التي يستدعيها Gradio."""
    if pdf_file is None:
        return "لم يتم رفع ملف."
    try:
        pdf_bytes = pdf_file.read() if hasattr(pdf_file, "read") else pdf_file
        limit = int(limit_pages) if limit_pages else 1  # صفحة واحدة افتراضًا للاختبار
        pages = pdf_to_images(pdf_bytes, dpi=int(dpi), max_pages=limit)
        if not pages:
            return "لا توجد صفحات."
        out = []
        for idx, img in pages:
            txt = ocr_page_with_horoof(img)
            out.append(f"--- صفحة {idx} ---\n{txt}")
        return "\n\n".join(out)
    except AssertionError as ae:
        return f"⚠️ {ae}"
    except Exception as e:
        traceback.print_exc()
        return f"حدث خطأ: {repr(e)}"

with gr.Blocks(title="Horoof OCR (ZeroGPU)") as demo:
    gr.Markdown("### Horoof OCR على ZeroGPU (Qwen2-VL) — بدون bitsandbytes/torchvision.")
    pdf_in = gr.File(label="ارفع ملف PDF", file_types=[".pdf"], type="binary")
    dpi = gr.Slider(150, 300, value=220, step=10, label="دقة التحويل (DPI)")
    limit_pages = gr.Number(value=1, precision=0, label="عدد الصفحات (للاختبار؛ زِد لاحقًا)")
    run_btn = gr.Button("بدء التحويل")
    out = gr.Textbox(label="النص المستخرج", lines=24)

    demo.queue()
    run_btn.click(fn=ocr_pdf, inputs=[pdf_in, dpi, limit_pages], outputs=out, api_name="ocr_pdf")

if __name__ == "__main__":
    demo.launch()