Spaces:
				
			
			
	
			
			
		Runtime error
		
	
	
	
			
			
	
	
	
	
		
		
		Runtime error
		
	| import os | |
| import io | |
| import traceback | |
| import logging | |
| import torch | |
| import gradio as gr | |
| import fitz # PyMuPDF | |
| from PIL import Image | |
| import spaces # للـ ZeroGPU | |
| # إعداد نظام التسجيل | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| # تعطيل مسار الفيديو داخل Transformers | |
| os.environ["TRANSFORMERS_NO_TORCHVISION"] = "1" | |
| # أسماء النماذج | |
| BASE_MODEL = os.environ.get("BASE_MODEL", "Qwen/Qwen2-VL-2B-Instruct") | |
| HOROOF_ADAPTER = os.environ.get("HOROOF_MODEL", "NaserNajeh/Horoof") | |
| # متغيرات النموذج العامة | |
| _model = None | |
| _tokenizer = None | |
| _img_proc = None | |
| _model_loaded = False | |
| def check_gpu_availability(): | |
| """التحقق من توفر GPU وطباعة معلومات النظام""" | |
| if not torch.cuda.is_available(): | |
| raise AssertionError("هذه النسخة تتطلب GPU (CUDA) مفعّل على الـSpace.") | |
| device_name = torch.cuda.get_device_name(0) | |
| memory_gb = torch.cuda.get_device_properties(0).total_memory / 1024**3 | |
| logger.info(f"GPU متاح: {device_name}") | |
| logger.info(f"ذاكرة GPU: {memory_gb:.1f} GB") | |
| return device_name, memory_gb | |
| def clear_gpu_cache(): | |
| """تنظيف ذاكرة GPU""" | |
| if torch.cuda.is_available(): | |
| torch.cuda.empty_cache() | |
| torch.cuda.synchronize() | |
| def load_model_merged(): | |
| """ | |
| تحميل النموذج مع تحسينات إضافية وإدارة أفضل للذاكرة | |
| """ | |
| global _model, _tokenizer, _img_proc, _model_loaded | |
| if _model_loaded and _model is not None: | |
| logger.info("النموذج محمّل بالفعل") | |
| return | |
| try: | |
| # التحقق من GPU | |
| device_name, memory_gb = check_gpu_availability() | |
| # استيراد المكتبات | |
| from transformers import ( | |
| Qwen2VLForConditionalGeneration, | |
| AutoTokenizer, | |
| Qwen2VLImageProcessor | |
| ) | |
| from peft import PeftModel | |
| logger.info("بدء تحميل النموذج...") | |
| # تحميل Tokenizer و ImageProcessor | |
| logger.info("تحميل المعالجات...") | |
| _tokenizer = AutoTokenizer.from_pretrained( | |
| BASE_MODEL, | |
| trust_remote_code=False, | |
| use_fast=True # تسريع التوكين | |
| ) | |
| _img_proc = Qwen2VLImageProcessor.from_pretrained( | |
| BASE_MODEL, | |
| trust_remote_code=False | |
| ) | |
| # تحميل النموذج الأساسي مع تحسينات الذاكرة | |
| logger.info(f"تحميل النموذج الأساسي: {BASE_MODEL}") | |
| # تحسين تحميل النموذج حسب حجم الذاكرة | |
| if memory_gb >= 40: # H100 أو A100 | |
| model_kwargs = { | |
| "torch_dtype": torch.float16, | |
| "device_map": "auto", | |
| "low_cpu_mem_usage": True | |
| } | |
| else: # GPUs أصغر | |
| model_kwargs = { | |
| "torch_dtype": torch.float16, | |
| "low_cpu_mem_usage": True | |
| } | |
| base = Qwen2VLForConditionalGeneration.from_pretrained( | |
| BASE_MODEL, | |
| **model_kwargs | |
| ) | |
| # نقل إلى GPU إذا لم يتم تلقائياً | |
| if not hasattr(base, 'device') or str(base.device) == 'cpu': | |
| base = base.to("cuda") | |
| # تحميل ودمج LoRA | |
| logger.info(f"تحميل LoRA adapter: {HOROOF_ADAPTER}") | |
| peft_model = PeftModel.from_pretrained(base, HOROOF_ADAPTER) | |
| logger.info("دمج LoRA مع النموذج الأساسي...") | |
| _model = peft_model.merge_and_unload() | |
| # التأكد من وجود النموذج على GPU | |
| if not str(_model.device).startswith('cuda'): | |
| _model = _model.to("cuda") | |
| # تحسين للاستنتاج | |
| _model.eval() | |
| # تمكين optimizations | |
| if hasattr(_model, 'half'): | |
| _model = _model.half() | |
| _model_loaded = True | |
| logger.info("تم تحميل النموذج بنجاح!") | |
| # طباعة معلومات الذاكرة | |
| if torch.cuda.is_available(): | |
| allocated = torch.cuda.memory_allocated() / 1024**3 | |
| logger.info(f"ذاكرة GPU المستخدمة: {allocated:.2f} GB") | |
| except Exception as e: | |
| logger.error(f"خطأ في تحميل النموذج: {str(e)}") | |
| logger.error(traceback.format_exc()) | |
| raise RuntimeError(f"تعذّر تحميل النموذج: {e}") | |
| def pdf_to_images(pdf_bytes: bytes, dpi: int = 220, max_pages: int = 0): | |
| """ | |
| تحويل PDF إلى صور PIL مع تحسينات | |
| """ | |
| try: | |
| pages_imgs = [] | |
| doc = fitz.open(stream=pdf_bytes, filetype="pdf") | |
| total = doc.page_count | |
| logger.info(f"عدد صفحات PDF: {total}") | |
| # تحديد عدد الصفحات للمعالجة | |
| n_pages = total if (not max_pages or max_pages <= 0) else min(max_pages, total) | |
| for i in range(n_pages): | |
| try: | |
| page = doc.load_page(i) | |
| # تحسين جودة التحويل | |
| mat = fitz.Matrix(dpi/72, dpi/72) | |
| pix = page.get_pixmap(matrix=mat, alpha=False) | |
| # تحويل لـ PIL Image | |
| img_data = pix.samples | |
| img = Image.frombytes("RGB", [pix.width, pix.height], img_data) | |
| # تحسين حجم الصورة إذا كانت كبيرة جداً | |
| max_dimension = 2048 | |
| if max(img.size) > max_dimension: | |
| img.thumbnail((max_dimension, max_dimension), Image.Resampling.LANCZOS) | |
| logger.info(f"تم تصغير الصفحة {i+1} إلى {img.size}") | |
| pages_imgs.append((i + 1, img)) | |
| logger.info(f"تم تحويل الصفحة {i+1}/{n_pages}") | |
| except Exception as e: | |
| logger.error(f"خطأ في تحويل الصفحة {i+1}: {str(e)}") | |
| continue | |
| doc.close() | |
| logger.info(f"تم تحويل {len(pages_imgs)} صفحة بنجاح") | |
| return pages_imgs | |
| except Exception as e: | |
| logger.error(f"خطأ في فتح PDF: {str(e)}") | |
| raise | |
| def ocr_page_gpu(pil_img: Image.Image, max_new_tokens: int = 1200) -> str: | |
| """ | |
| OCR لصفحة واحدة مع تحسينات الأداء | |
| """ | |
| try: | |
| # التأكد من تحميل النموذج | |
| load_model_merged() | |
| # تنظيف الذاكرة قبل المعالجة | |
| clear_gpu_cache() | |
| # رسالة المحادثة | |
| messages = [ | |
| { | |
| "role": "user", | |
| "content": [ | |
| {"type": "image", "image": pil_img}, | |
| {"type": "text", "text": "اقرأ النص العربي في الصورة كما هو دون أي تعديل أو تفسير."}, | |
| ], | |
| } | |
| ] | |
| # توكين النص | |
| tok = _tokenizer.apply_chat_template( | |
| messages, | |
| add_generation_prompt=True, | |
| tokenize=True, | |
| return_tensors="pt" | |
| ) | |
| # معالجة الصورة | |
| vis = _img_proc(images=[pil_img], return_tensors="pt") | |
| # تجهيز المدخلات | |
| inputs = {"input_ids": tok} | |
| inputs.update(vis) | |
| # نقل إلى GPU | |
| for k, v in inputs.items(): | |
| if hasattr(v, "to"): | |
| inputs[k] = v.to("cuda") | |
| # التوليد مع تحسينات | |
| with torch.inference_mode(): | |
| output_ids = _model.generate( | |
| **inputs, | |
| max_new_tokens=max_new_tokens, | |
| do_sample=False, # للحصول على نتائج مستقرة | |
| temperature=0.1, | |
| pad_token_id=_tokenizer.eos_token_id, | |
| use_cache=True | |
| ) | |
| # فك التشفير | |
| generated_part = output_ids[0][len(inputs["input_ids"][0]):] | |
| result = _tokenizer.decode(generated_part, skip_special_tokens=True) | |
| # تنظيف النتيجة | |
| result = result.strip() | |
| # تنظيف الذاكرة بعد المعالجة | |
| clear_gpu_cache() | |
| return result if result else "لم يتم استخراج أي نص" | |
| except Exception as e: | |
| logger.error(f"خطأ في OCR: {str(e)}") | |
| clear_gpu_cache() | |
| return f"خطأ في معالجة الصورة: {str(e)}" | |
| # ضروري لـ ZeroGPU | |
| def ocr_pdf(pdf_file, dpi, limit_pages): | |
| """الدالة الرئيسية مع تحسينات إضافية""" | |
| if pdf_file is None: | |
| return "❌ لم يتم رفع ملف PDF." | |
| try: | |
| logger.info("بدء معالجة PDF...") | |
| # قراءة البيانات | |
| if hasattr(pdf_file, 'read'): | |
| pdf_bytes = pdf_file.read() | |
| else: | |
| pdf_bytes = pdf_file | |
| if not pdf_bytes: | |
| return "❌ الملف فارغ أو تالف." | |
| # تحويل إلى صور | |
| limit = max(1, int(limit_pages)) if limit_pages else 1 | |
| dpi_val = max(150, min(300, int(dpi))) # تحديد نطاق DPI | |
| logger.info(f"تحويل {limit} صفحة بدقة {dpi_val} DPI...") | |
| pages = pdf_to_images(pdf_bytes, dpi=dpi_val, max_pages=limit) | |
| if not pages: | |
| return "❌ لا توجد صفحات صالحة للمعالجة." | |
| # معالجة OCR | |
| results = [] | |
| total_pages = len(pages) | |
| for i, (page_num, img) in enumerate(pages, 1): | |
| try: | |
| logger.info(f"معالجة الصفحة {page_num} ({i}/{total_pages})...") | |
| # OCR للصفحة | |
| text = ocr_page_gpu(img) | |
| if text and text.strip(): | |
| results.append(f"📄 **صفحة {page_num}**\n{'-'*50}\n{text}") | |
| else: | |
| results.append(f"📄 **صفحة {page_num}**\n{'-'*50}\n⚠️ لم يتم استخراج نص من هذه الصفحة") | |
| except Exception as e: | |
| logger.error(f"خطأ في معالجة الصفحة {page_num}: {str(e)}") | |
| results.append(f"📄 **صفحة {page_num}**\n{'-'*50}\n❌ خطأ في المعالجة: {str(e)}") | |
| if not results: | |
| return "❌ لم يتم استخراج أي نص من الملف." | |
| final_result = "\n\n".join(results) | |
| logger.info(f"تمت معالجة {len(results)} صفحة بنجاح") | |
| return final_result | |
| except Exception as e: | |
| logger.error(f"خطأ عام في المعالجة: {str(e)}") | |
| logger.error(traceback.format_exc()) | |
| return f"❌ حدث خطأ: {str(e)}" | |
| # إنشاء واجهة Gradio | |
| with gr.Blocks( | |
| title="Horoof OCR (H200 GPU)", | |
| theme=gr.themes.Soft(), | |
| css=""" | |
| .gradio-container { | |
| font-family: 'Arial', sans-serif; | |
| } | |
| .output-text { | |
| font-family: 'Courier New', monospace; | |
| line-height: 1.6; | |
| } | |
| """ | |
| ) as demo: | |
| gr.Markdown(""" | |
| # 🔤 Horoof OCR - استخراج النصوص العربية | |
| **النموذج**: Qwen2-VL-2B + Horoof LoRA | |
| **المعالج**: H200 GPU | |
| **الدعم**: PDF → نص عربي عالي الجودة | |
| --- | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| pdf_input = gr.File( | |
| label="📁 ارفع ملف PDF", | |
| file_types=[".pdf"], | |
| type="binary" | |
| ) | |
| dpi_slider = gr.Slider( | |
| minimum=150, | |
| maximum=300, | |
| value=220, | |
| step=10, | |
| label="🎯 دقة التحويل (DPI)", | |
| info="دقة أعلى = جودة أفضل + وقت أطول" | |
| ) | |
| pages_limit = gr.Number( | |
| value=2, | |
| minimum=1, | |
| maximum=10, | |
| precision=0, | |
| label="📊 عدد الصفحات المراد معالجتها", | |
| info="ابدأ بعدد قليل للاختبار" | |
| ) | |
| process_btn = gr.Button( | |
| "🚀 بدء الاستخراج", | |
| variant="primary", | |
| size="lg" | |
| ) | |
| with gr.Column(scale=2): | |
| output_text = gr.Textbox( | |
| label="📝 النص المستخرج", | |
| lines=25, | |
| max_lines=30, | |
| elem_classes=["output-text"], | |
| placeholder="سيظهر النص المستخرج هنا...", | |
| show_copy_button=True | |
| ) | |
| gr.Markdown(""" | |
| --- | |
| ### 💡 نصائح للحصول على أفضل النتائج: | |
| - **جودة الملف**: تأكد من أن PDF واضح وقابل للقراءة | |
| - **DPI**: استخدم 220-250 للنصوص العادية، 280-300 للخطوط الصغيرة | |
| - **عدد الصفحات**: ابدأ بصفحة أو اثنتين للاختبار | |
| - **أنواع النصوص**: يعمل بشكل ممتاز مع النصوص العربية المطبوعة والمكتوبة بوضوح | |
| """) | |
| # ربط الأحداث | |
| process_btn.click( | |
| fn=ocr_pdf, | |
| inputs=[pdf_input, dpi_slider, pages_limit], | |
| outputs=output_text, | |
| api_name="ocr_pdf" | |
| ) | |
| # إعداد الـ queue | |
| demo.queue( | |
| concurrency_count=2, # عدد العمليات المتزامنة | |
| max_size=10 # حد أقصى للطابور | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False, | |
| show_error=True, | |
| quiet=False | |
| ) |