NaserNajeh commited on
Commit
c8b8e71
·
verified ·
1 Parent(s): ce0aa31

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +89 -34
app.py CHANGED
@@ -1,12 +1,15 @@
1
- import os, io
2
  import gradio as gr
3
  import fitz # PyMuPDF
4
  from PIL import Image
5
  import numpy as np
6
 
7
- # ===== EasyOCR (CPU مجاني) =====
 
 
8
  import easyocr
9
  _EASY_READER = None
 
10
  def get_easy_reader():
11
  global _EASY_READER
12
  if _EASY_READER is None:
@@ -19,70 +22,114 @@ def ocr_easyocr(pil_img: Image.Image) -> str:
19
  lines = reader.readtext(arr, detail=0, paragraph=True)
20
  return "\n".join([x.strip() for x in lines if x and x.strip()])
21
 
22
- # ===== Inference API (يستهلك اعتمادات PRO بدل دقائق GPU) =====
 
 
 
23
  from huggingface_hub import InferenceClient
24
  _INFER_CLIENT = None
 
25
  INFER_MODEL = os.environ.get("INFER_MODEL", "Qwen/Qwen2-VL-2B-Instruct")
26
 
27
  def get_infer_client():
 
28
  global _INFER_CLIENT
29
  if _INFER_CLIENT is None:
30
- token = os.environ.get("HF_TOKEN") # أضِفه من Settings → Secrets عند الحاجة
31
- _INFER_CLIENT = InferenceClient(model=INFER_MODEL, token=token)
 
 
32
  return _INFER_CLIENT
33
 
34
  def ocr_infer_api(pil_img: Image.Image) -> str:
35
- client = get_infer_client()
36
- buf = io.BytesIO()
37
- pil_img.save(buf, format="PNG"); buf.seek(0)
38
- # ملاحظة: بعض النماذج تدعم image_to_text مباشرة، والبعض عبر chat.completions.
39
- # نجرب image_to_text أولًا:
40
  try:
41
- txt = client.image_to_text(image=buf, prompt="اقرأ النص العربي كما هو دون أي تعديل.")
42
- return txt.strip()
43
- except Exception:
44
- # بديل عام عبر واجهة chat (إن كانت مدعومة)
45
- msgs = [
46
- {"role": "system", "content": "You are an OCR assistant. Return only the text."},
 
 
 
 
47
  {
48
  "role": "user",
49
  "content": [
50
- {"type": "input_text", "text": "Extract Arabic text exactly as is."},
51
- {"type": "image", "image": buf.getvalue()},
52
  ],
53
  },
54
  ]
55
- resp = client.chat.completions.create(messages=msgs, max_tokens=2048)
56
- return resp.choices[0].message.content.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
- # ===== Horoof (Qari محليًا) — يحتاج CUDA ليعمل بكفاءة =====
59
- import torch
60
- HAS_CUDA = torch.cuda.is_available()
61
  _HOROOF_MODEL = None
62
  _HOROOF_PROC = None
 
63
  HOROOF_MODEL_NAME = os.environ.get("HOROOF_MODEL", "NaserNajeh/Horoof")
64
 
65
  def ensure_horoof_loaded():
 
66
  global _HOROOF_MODEL, _HOROOF_PROC
67
  if _HOROOF_MODEL is None:
 
 
 
 
68
  from transformers import Qwen2VLForConditionalGeneration, AutoProcessor
69
- device = "cuda" if HAS_CUDA else "cpu"
70
  _HOROOF_MODEL = Qwen2VLForConditionalGeneration.from_pretrained(
71
  HOROOF_MODEL_NAME, torch_dtype="auto"
72
- ).to(device)
73
  _HOROOF_PROC = AutoProcessor.from_pretrained(HOROOF_MODEL_NAME)
74
 
75
  def ocr_horoof(pil_img: Image.Image) -> str:
76
- if not HAS_CUDA:
77
  return "⚠️ خيار Horoof المحلي يتطلب GPU (CUDA). اختر EasyOCR أو Inference API."
78
  ensure_horoof_loaded()
79
- device = "cuda"
80
- inputs = _HOROOF_PROC(images=pil_img, return_tensors="pt").to(device)
81
  gen = _HOROOF_MODEL.generate(**inputs, max_new_tokens=1800)
82
  text = _HOROOF_PROC.batch_decode(gen, skip_special_tokens=True)[0]
83
- return text.strip()
 
84
 
85
- # ===== تحويل PDF إلى صور =====
 
 
86
  def pdf_to_images(pdf_bytes: bytes, dpi: int = 220, max_pages: int = 0):
87
  pages_imgs = []
88
  doc = fitz.open(stream=pdf_bytes, filetype="pdf")
@@ -96,7 +143,10 @@ def pdf_to_images(pdf_bytes: bytes, dpi: int = 220, max_pages: int = 0):
96
  doc.close()
97
  return pages_imgs
98
 
99
- # ===== الدالة الرئيسية مع اختيار الـBackend =====
 
 
 
100
  BACKENDS = ["EasyOCR (CPU - مجاني)", "Inference API (Qwen2-VL)", "Horoof (محلي - يتطلب GPU)"]
101
 
102
  def ocr_pdf(pdf_file, dpi, limit_pages, backend):
@@ -110,15 +160,20 @@ def ocr_pdf(pdf_file, dpi, limit_pages, backend):
110
  if backend.startswith("EasyOCR"):
111
  txt = ocr_easyocr(img)
112
  elif backend.startswith("Inference API"):
113
- # تأكد من وجود HF_TOKEN لو الـSpace Private
114
  txt = ocr_infer_api(img)
115
  else:
116
  txt = ocr_horoof(img)
117
  results.append(f"--- صفحة {idx} ---\n{txt}")
118
  return "\n\n".join(results) if results else "لا توجد صفحات."
119
  except Exception as e:
120
- return f"حدث خطأ: {e}"
 
 
 
121
 
 
 
 
122
  with gr.Blocks(title="Horoof Hybrid OCR") as demo:
123
  gr.Markdown("### OCR عربي هجين: مجاني على CPU (EasyOCR)، أو عبر Inference API، أو Horoof محليًا على GPU.")
124
  with gr.Row():
@@ -130,7 +185,7 @@ with gr.Blocks(title="Horoof Hybrid OCR") as demo:
130
  run_btn = gr.Button("بدء التحويل")
131
  out = gr.Textbox(label="النص المستخرج", lines=24)
132
 
133
- # API ثابت لاستدعاء الـSpace كـ خدمة
134
  run_btn.click(fn=ocr_pdf, inputs=[pdf_in, dpi, limit_pages, backend], outputs=out, api_name="ocr_pdf")
135
 
136
  if __name__ == "__main__":
 
1
+ import os, io, base64, traceback
2
  import gradio as gr
3
  import fitz # PyMuPDF
4
  from PIL import Image
5
  import numpy as np
6
 
7
+ # =======================
8
+ # 1) EasyOCR (CPU - مجاني)
9
+ # =======================
10
  import easyocr
11
  _EASY_READER = None
12
+
13
  def get_easy_reader():
14
  global _EASY_READER
15
  if _EASY_READER is None:
 
22
  lines = reader.readtext(arr, detail=0, paragraph=True)
23
  return "\n".join([x.strip() for x in lines if x and x.strip()])
24
 
25
+
26
+ # ===============================================
27
+ # 2) Inference API (يستهلك اعتمادات PRO بدل دقائق GPU)
28
+ # ===============================================
29
  from huggingface_hub import InferenceClient
30
  _INFER_CLIENT = None
31
+ # يمكن تغيير الموديل من Secrets → Variables بوضع INFER_MODEL، الافتراضي:
32
  INFER_MODEL = os.environ.get("INFER_MODEL", "Qwen/Qwen2-VL-2B-Instruct")
33
 
34
  def get_infer_client():
35
+ """تهيئة عميل الاستدلال مع مهلة أطول ورسالة واضحة إن غاب التوكين."""
36
  global _INFER_CLIENT
37
  if _INFER_CLIENT is None:
38
+ token = os.environ.get("HF_TOKEN")
39
+ if not token:
40
+ raise RuntimeError("لا يوجد HF_TOKEN في Secrets. أضِفه من Settings → Variables and secrets.")
41
+ _INFER_CLIENT = InferenceClient(model=INFER_MODEL, token=token, timeout=120)
42
  return _INFER_CLIENT
43
 
44
  def ocr_infer_api(pil_img: Image.Image) -> str:
45
+ """نحاول أولاً واجهة chat.completions بتمرير bytes، وإن فشلت نستخدم data URI."""
 
 
 
 
46
  try:
47
+ client = get_infer_client()
48
+
49
+ buf = io.BytesIO()
50
+ pil_img.save(buf, format="PNG")
51
+ raw_bytes = buf.getvalue()
52
+ b64 = base64.b64encode(raw_bytes).decode("utf-8")
53
+
54
+ # محاولة 1: تمريـر الصورة كـ bytes (بعض النماذج تدعم ذلك)
55
+ messages = [
56
+ {"role": "system", "content": "You are an OCR assistant. Return ONLY the Arabic text as-is."},
57
  {
58
  "role": "user",
59
  "content": [
60
+ {"type": "input_text", "text": "Extract Arabic text exactly as-is, no extra commentary."},
61
+ {"type": "image", "image": raw_bytes},
62
  ],
63
  },
64
  ]
65
+ try:
66
+ resp = client.chat.completions.create(messages=messages, max_tokens=2048)
67
+ txt = resp.choices[0].message.content or ""
68
+ return txt.strip()
69
+ except Exception:
70
+ # محاولة 2: تمريـر الصورة كـ data URI عبر image_url
71
+ messages_fallback = [
72
+ {"role": "system", "content": "You are an OCR assistant. Return ONLY the Arabic text as-is."},
73
+ {
74
+ "role": "user",
75
+ "content": [
76
+ {"type": "input_text", "text": "Extract Arabic text exactly as-is, no extra commentary."},
77
+ {"type": "image_url", "image_url": f"data:image/png;base64,{b64}"},
78
+ ],
79
+ },
80
+ ]
81
+ resp = client.chat.completions.create(messages=messages_fallback, max_tokens=2048)
82
+ txt = resp.choices[0].message.content or ""
83
+ return txt.strip()
84
+
85
+ except Exception as e:
86
+ # إظهار رسالة مفيدة بدل سطر فارغ
87
+ return f"حدث خطأ أثناء استدعاء Inference API: {repr(e)}"
88
+
89
+
90
+ # =====================================================
91
+ # 3) Horoof (Qari محليًا) — يتطلب CUDA لعمل فعّال على الـSpace
92
+ # =====================================================
93
+ try:
94
+ import torch
95
+ HAS_TORCH = True
96
+ HAS_CUDA = torch.cuda.is_available()
97
+ except Exception:
98
+ HAS_TORCH = False
99
+ HAS_CUDA = False
100
 
 
 
 
101
  _HOROOF_MODEL = None
102
  _HOROOF_PROC = None
103
+ # يمكن تغيير اسم نموذجك من Secrets → Variables بوضع HOROOF_MODEL، الافتراضي:
104
  HOROOF_MODEL_NAME = os.environ.get("HOROOF_MODEL", "NaserNajeh/Horoof")
105
 
106
  def ensure_horoof_loaded():
107
+ """تحميل نموذج Horoof عند الحاجة فقط (Lazy) لتقليل زمن الإقلاع على CPU-basic."""
108
  global _HOROOF_MODEL, _HOROOF_PROC
109
  if _HOROOF_MODEL is None:
110
+ if not HAS_TORCH:
111
+ raise RuntimeError("حزمة torch غير متاحة. ثبّت torch أو استخدم Backend آخر.")
112
+ if not HAS_CUDA:
113
+ raise RuntimeError("خيار Horoof المحلي يتطلب GPU (CUDA). اختر EasyOCR أو Inference API.")
114
  from transformers import Qwen2VLForConditionalGeneration, AutoProcessor
 
115
  _HOROOF_MODEL = Qwen2VLForConditionalGeneration.from_pretrained(
116
  HOROOF_MODEL_NAME, torch_dtype="auto"
117
+ ).to("cuda")
118
  _HOROOF_PROC = AutoProcessor.from_pretrained(HOROOF_MODEL_NAME)
119
 
120
  def ocr_horoof(pil_img: Image.Image) -> str:
121
+ if not HAS_TORCH or not HAS_CUDA:
122
  return "⚠️ خيار Horoof المحلي يتطلب GPU (CUDA). اختر EasyOCR أو Inference API."
123
  ensure_horoof_loaded()
124
+ inputs = _HOROOF_PROC(images=pil_img, return_tensors="pt").to("cuda")
 
125
  gen = _HOROOF_MODEL.generate(**inputs, max_new_tokens=1800)
126
  text = _HOROOF_PROC.batch_decode(gen, skip_special_tokens=True)[0]
127
+ return (text or "").strip()
128
+
129
 
130
+ # ===========================
131
+ # أداة: تحويل PDF إلى صور
132
+ # ===========================
133
  def pdf_to_images(pdf_bytes: bytes, dpi: int = 220, max_pages: int = 0):
134
  pages_imgs = []
135
  doc = fitz.open(stream=pdf_bytes, filetype="pdf")
 
143
  doc.close()
144
  return pages_imgs
145
 
146
+
147
+ # =========================================
148
+ # الدالة الرئيسية + اختيار الـBackend
149
+ # =========================================
150
  BACKENDS = ["EasyOCR (CPU - مجاني)", "Inference API (Qwen2-VL)", "Horoof (محلي - يتطلب GPU)"]
151
 
152
  def ocr_pdf(pdf_file, dpi, limit_pages, backend):
 
160
  if backend.startswith("EasyOCR"):
161
  txt = ocr_easyocr(img)
162
  elif backend.startswith("Inference API"):
 
163
  txt = ocr_infer_api(img)
164
  else:
165
  txt = ocr_horoof(img)
166
  results.append(f"--- صفحة {idx} ---\n{txt}")
167
  return "\n\n".join(results) if results else "لا توجد صفحات."
168
  except Exception as e:
169
+ # طباعة أثر الخطأ للمساعدة في التشخيص داخل Logs
170
+ traceback.print_exc()
171
+ return f"حدث خطأ: {repr(e)}"
172
+
173
 
174
+ # =======================
175
+ # واجهة Gradio + API Name
176
+ # =======================
177
  with gr.Blocks(title="Horoof Hybrid OCR") as demo:
178
  gr.Markdown("### OCR عربي هجين: مجاني على CPU (EasyOCR)، أو عبر Inference API، أو Horoof محليًا على GPU.")
179
  with gr.Row():
 
185
  run_btn = gr.Button("بدء التحويل")
186
  out = gr.Textbox(label="النص المستخرج", lines=24)
187
 
188
+ # api_name يجعل الـSpace قابلاً للاستدعاء كـ API:
189
  run_btn.click(fn=ocr_pdf, inputs=[pdf_in, dpi, limit_pages, backend], outputs=out, api_name="ocr_pdf")
190
 
191
  if __name__ == "__main__":