Spaces:
Runtime error
Runtime error
| 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() | |
| # ضروري ل 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() | |