Views
No views yet

| Performance Metric | v3 | v4 | Performance Delta |
|---|---|---|---|
| ⏱️ Time per Image | 0.31 seconds | 0.25 seconds | +25% Faster |
| 🚀 Images per Second | 3.23 images | 4 images | 20% throughput |
| ⚡ Printed Performance | 70% | 90% | 30% percentage points |
| 🚀 Page per Second | seconds | 3.5 seconds | Faster Average of 100 samples |
![]() | ![]() |
![]() | ![]() |
1
2import os
3import torch
4from PIL import Image
5from transformers import AutoProcessor, Qwen3_5ForConditionalGeneration
6from qwen_vl_utils import process_vision_info
7
8# ==================== ⚙️ إعدادات الجهاز ====================
9device = "cuda" if torch.cuda.is_available() else "cpu"
10dtype = torch.float16 if device == "cuda" else torch.float32
11
12# ==================== 🔄 تحميل النموذج ====================
13print("[INFO] Loading model...")
14model_path = "sherif1313/Arabic-Qwen3.5-OCR-v4" # ← غيّر لمسار نموذجك
15
16processor = AutoProcessor.from_pretrained(model_path, trust_remote_code=True)
17model = Qwen3_5ForConditionalGeneration.from_pretrained(
18 model_path,
19 dtype=dtype,
20 device_map="auto" if device == "cuda" else None,
21 trust_remote_code=True
22)
23model.eval()
24print("[INFO] Model loaded!")
25
26# ==================== 🔍 دالة الاستدلال (تُعرّف أولاً!) ====================
27def extract_text(image_path: str, prompt: str = None) -> str:
28 """استخراج النص من صورة واحدة"""
29 if prompt is None:
30 prompt = "اقرأ النص في هذه الصورة كاملاً من البداية إلى النهاية."
31
32 image = Image.open(image_path).convert("RGB")
33
34 # ضبط الأبعاد لمضاعفات 64
35 w, h = image.size
36 new_w = ((w + 63) // 64) * 64
37 new_h = ((h + 63) // 64) * 64
38 image = image.resize((new_w, new_h), Image.LANCZOS)
39
40 messages = [{
41 "role": "user",
42 "content": [
43 {"type": "image", "image": image},
44 {"type": "text", "text": prompt}
45 ]
46 }]
47
48 text_input = processor.apply_chat_template(
49 messages, tokenize=False, add_generation_prompt=True
50 )
51 image_inputs, _ = process_vision_info(messages)
52
53 inputs = processor(
54 text=[text_input],
55 images=image_inputs,
56 padding=True,
57 return_tensors="pt"
58 ).to(device)
59
60 with torch.no_grad():
61 generated_ids = model.generate(
62 **inputs,
63 max_new_tokens=512,
64 do_sample=False,
65 repetition_penalty=1.2,
66 no_repeat_ngram_size=3,
67 pad_token_id=processor.tokenizer.pad_token_id,
68 eos_token_id=processor.tokenizer.eos_token_id,
69 )
70
71 input_len = inputs.input_ids.shape[1]
72 output_text = processor.batch_decode(
73 generated_ids[:, input_len:],
74 skip_special_tokens=True,
75 clean_up_tokenization_spaces=False
76 )[0]
77
78 return output_text.strip()
79
80# ==================== 🚀 نقطة الدخول (تُستدعى بعد تعريف الدالة) ====================
81if __name__ == "__main__":
82 # ✅ الآن يمكن استدعاء extract_text لأنها مُعرّفة أعلاه
83 image_path = "/home/sheriff/Downloads/PIC.png"
84
85 if os.path.exists(image_path):
86 print(f"🔍 Processing: {image_path}")
87 result = extract_text(image_path)
88 print(f"📝 Extracted Text:\n{result}")
89 else:
90 print(f"❌ File not found: {image_path}")1import os
2import time
3import torch
4from PIL import Image
5import gradio as gr
6from transformers import AutoProcessor, Qwen3_5ForConditionalGeneration
7from qwen_vl_utils import process_vision_info
8
9# ==================== ⚙️ إعدادات الجهاز ====================
10if torch.cuda.is_available():
11 device = "cuda"
12 dtype = torch.float16
13 print(f"✅ Using GPU: {torch.cuda.get_device_name(0)}")
14elif torch.backends.mps.is_available():
15 device = "mps"
16 dtype = torch.float16
17 print("✅ Using Apple Silicon (MPS)")
18else:
19 device = "cpu"
20 dtype = torch.float32
21 print("⚠️ Using CPU (slower inference)")
22
23print(f"[INFO] Device: {device} | Dtype: {dtype}")
24
25# ==================== 🔄 تحميل النموذج ====================
26def load_model():
27 """تحميل النموذج والمعالج مع إدارة الذاكرة"""
28 model_path = os.getenv("MODEL_PATH", "sherif1313/Arabic-Qwen3.5-OCR-v4")
29
30 print(f"[INFO] Loading model from: {model_path}")
31
32 processor = AutoProcessor.from_pretrained(model_path, trust_remote_code=True)
33
34 model = Qwen3_5ForConditionalGeneration.from_pretrained(
35 model_path,
36 dtype=dtype,
37 device_map="auto" if device == "cuda" else None,
38 trust_remote_code=True,
39 low_cpu_mem_usage=True,
40 )
41
42 model.eval()
43 print("[INFO] Model loaded successfully!")
44 return model, processor
45
46# تحميل عالمي (يتم مرة واحدة عند بدء التطبيق)
47try:
48 model, processor = load_model()
49except Exception as e:
50 print(f"[ERROR] Failed to load model: {e}")
51 model = None
52 processor = None
53
54# ==================== 🧹 دوال مساعدة ====================
55def prepare_image(image: Image.Image, max_size: int = 768) -> Image.Image:
56 """تحضير الصورة: ضغط + ضبط الأبعاد لمضاعفات 64"""
57 if max(image.size) > max_size:
58 image.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
59
60 w, h = image.size
61 new_w = ((w + 63) // 64) * 64
62 new_h = ((h + 63) // 64) * 64
63 if (new_w, new_h) != image.size:
64 image = image.resize((new_w, new_h), Image.Resampling.LANCZOS)
65
66 return image
67
68def clean_output(text: str, max_repetitions: int = 2) -> str:
69 """تنظيف التكرار في المخرجات"""
70 if not text:
71 return text
72
73 import re
74 text = re.sub(r'(.)\1{4,}', r'\1\1\1', text)
75
76 lines = text.strip().split('\n')
77 cleaned = []
78 seen = {}
79 for line in lines:
80 line_stripped = line.strip()
81 if not line_stripped:
82 continue
83 count = seen.get(line_stripped, 0) + 1
84 if count <= max_repetitions:
85 cleaned.append(line)
86 seen[line_stripped] = count
87
88 return '\n'.join(cleaned).strip()
89
90# ==================== 🔍 دالة الاستدلال ====================
91def extract_text(image, prompt: str = None) -> tuple[str, str]:
92 """استخراج النص من الصورة"""
93 if model is None or processor is None:
94 return "❌ Error: Model not loaded", "0.00"
95
96 if image is None:
97 return "⚠️ Please upload an image", "0.00"
98
99 start_time = time.time()
100
101 try:
102 if isinstance(image, str):
103 image_pil = Image.open(image).convert("RGB")
104 elif isinstance(image, Image.Image):
105 image_pil = image.convert("RGB")
106 else:
107 image_pil = Image.fromarray(image).convert("RGB")
108
109 image_pil = prepare_image(image_pil)
110
111 if prompt is None or not prompt.strip():
112 prompt = "اقرأ النص في هذه الصورة كاملاً من البداية إلى النهاية."
113
114 messages = [{
115 "role": "user",
116 "content": [
117 {"type": "image", "image": image_pil},
118 {"type": "text", "text": prompt}
119 ]
120 }]
121
122 text_input = processor.apply_chat_template(
123 messages, tokenize=False, add_generation_prompt=True
124 )
125 image_inputs, _ = process_vision_info(messages)
126
127 inputs = processor(
128 text=[text_input],
129 images=image_inputs,
130 padding=True,
131 return_tensors="pt"
132 ).to(device)
133
134 with torch.inference_mode():
135 generated_ids = model.generate(
136 **inputs,
137 max_new_tokens=512,
138 do_sample=False,
139 temperature=1.0,
140 repetition_penalty=1.2,
141 no_repeat_ngram_size=3,
142 pad_token_id=processor.tokenizer.pad_token_id,
143 eos_token_id=processor.tokenizer.eos_token_id,
144 )
145
146 input_len = inputs.input_ids.shape[1]
147 output_text = processor.batch_decode(
148 generated_ids[:, input_len:],
149 skip_special_tokens=True,
150 clean_up_tokenization_spaces=False
151 )[0]
152
153 output_text = clean_output(output_text.strip())
154
155 elapsed = time.time() - start_time
156
157 return output_text, f"{elapsed:.2f} seconds"
158
159 except torch.cuda.OutOfMemoryError:
160 torch.cuda.empty_cache()
161 return "❌ Out of Memory. Try a smaller image.", "0.00"
162 except Exception as e:
163 print(f"[ERROR] {e}")
164 import traceback
165 traceback.print_exc()
166 return f"❌ Error: {str(e)}", "0.00"
167
168# ==================== 🎨 واجهة Gradio ====================
169def create_interface():
170 """إنشاء واجهة المستخدم"""
171
172 with gr.Blocks(
173 title="Arabic OCR - Qwen3.5-0.8B",
174 theme=gr.themes.Soft(),
175 css="""
176 .header { text-align: center; margin-bottom: 20px; }
177 .output-box { min-height: 200px; }
178 """
179 ) as demo:
180
181 gr.Markdown("""
182 # 📝 Arabic Handwritten & Printed OCR V4
183 ### Powered by Qwen3.5-0.8B
184
185 Upload an image containing Arabic text, and the model will extract it.
186
187 ✨ **Features:**
188 - 🌍 Arabic support
189 - ✍️ Handwritten & printed text
190 - 🔤 Preserves diacritics (تشكيل)
191 - ⚡ Full precision (no quantization)
192 """, elem_classes="header")
193
194 with gr.Row():
195 with gr.Column(scale=1):
196 # ✅ تعريف المكونات أولاً
197 image_input = gr.Image(
198 label="📷 Upload Image",
199 type="pil",
200 height=300,
201 sources=["upload", "clipboard"]
202 )
203
204 prompt_input = gr.Textbox(
205 label="📝 Custom Prompt (Optional)",
206 placeholder="اقرأ النص في هذه الصورة...",
207 value="اقرأ النص في هذه الصورة كاملاً من البداية إلى النهاية.",
208 lines=2
209 )
210
211 submit_btn = gr.Button(
212 "🔍 Extract Text",
213 variant="primary",
214 size="lg"
215 )
216
217 # ✅ الأمثلة داخل الدالة - مسارات محلية فقط (لا روابط خارجية)
218 # لإضافة أمثلة، انسخ الصور إلى مجلد 'examples/' في مستودع الـ Space
219 # ثم استخدم: examples=[["examples/sample1.jpg"], ...]
220 gr.Examples(
221 label="📋 Examples (Optional)",
222 examples=[
223], # اتركها فارغة أو استخدم مسارات محلية
224 inputs=[image_input], # ✅ الآن يعمل لأن image_input مُعرّف أعلاه
225 cache_examples=False
226 )
227
228 with gr.Column(scale=1):
229 output_text = gr.Textbox(
230 label="📄 Extracted Text",
231 lines=12,
232 show_copy_button=True,
233 elem_classes="output-box"
234 )
235
236 time_output = gr.Textbox(
237 label="⏱️ Inference Time",
238 interactive=False,
239 value="-"
240 )
241
242 clear_btn = gr.Button("🗑️ Clear", variant="secondary")
243
244 # ✅ ربط الأحداث (بعد تعريف جميع المكونات)
245 submit_btn.click(
246 fn=extract_text,
247 inputs=[image_input, prompt_input],
248 outputs=[output_text, time_output]
249 )
250
251 clear_btn.click(
252 fn=lambda: (None, "", "-"),
253 inputs=[],
254 outputs=[image_input, prompt_input, time_output]
255 )
256
257 gr.Markdown("""
258 ### 💡 Tips for Best Results:
259 1. Use clear, well-lit images
260 2. Crop to the text region if possible
261 3. For handwritten text, ensure good contrast
262 4. Custom prompts can improve accuracy for specific formats
263 """)
264
265 return demo # ✅ إرجاع الـ demo
266
267# ==================== 🚀 نقطة الدخول ====================
268if __name__ == "__main__":
269 print("[INFO] Creating Gradio interface...")
270
271 demo = create_interface()
272
273 # إعدادات التشغيل لـ Spaces
274 demo.launch(
275 server_name="0.0.0.0",
276 server_port=int(os.getenv("PORT", 7860)),
277 share=False,
278 debug=os.getenv("DEBUG", "false").lower() == "true",
279 show_error=True
280 )1import os
2import sys
3import time
4import torch
5from PIL import Image
6from transformers import AutoProcessor, Qwen3_5ForConditionalGeneration
7from qwen_vl_utils import process_vision_info
8import fitz # PyMuPDF
9
10# ==================== ⚙️ إعدادات الجهاز ====================
11DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
12DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32
13print(f"[INFO] Using device: {DEVICE} | dtype: {DTYPE}")
14
15# ==================== 🔄 تحميل النموذج ====================
16def load_model(model_path: str):
17 """تحميل النموذج والمعالج"""
18 print(f"[INFO] Loading model from: {model_path}")
19
20 processor = AutoProcessor.from_pretrained(model_path, trust_remote_code=True)
21
22 model = Qwen3_5ForConditionalGeneration.from_pretrained(
23 model_path,
24 torch_dtype=DTYPE,
25 device_map="auto" if DEVICE == "cuda" else None,
26 trust_remote_code=True,
27 low_cpu_mem_usage=True
28 )
29 model.eval()
30 print("[INFO] ✅ Model loaded successfully!")
31 return model, processor
32
33# ==================== 🖼️ تحويل صفحة PDF إلى صورة ====================
34def pdf_page_to_image(pdf_path: str, page_num: int, dpi: int = 150) -> Image.Image:
35 """تحويل صفحة من ملف PDF إلى صورة PIL"""
36 doc = fitz.open(pdf_path)
37 page = doc[page_num]
38
39 # إعداد مصفوفة التكبير للدقة المطلوبة
40 zoom = dpi / 72 # 72 DPI هو الافتراضي في PDF
41 mat = fitz.Matrix(zoom, zoom)
42
43 # الحصول على الصورة
44 pix = page.get_pixmap(matrix=mat)
45 img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
46
47 doc.close()
48 return img
49
50# ==================== 🧹 تنظيف المخرجات من التكرار ====================
51def clean_output(text: str, max_repetitions: int = 2) -> str:
52 """إزالة التكرار المفرط في النص المستخرج"""
53 import re
54 if not text:
55 return text
56
57 # إزالة تكرار الحروف المفرط
58 text = re.sub(r'(.)\1{4,}', r'\1\1\1', text)
59
60 # إزالة تكرار الأسطر
61 lines = text.strip().split('\n')
62 cleaned = []
63 seen = {}
64 for line in lines:
65 line_stripped = line.strip()
66 if not line_stripped:
67 continue
68 count = seen.get(line_stripped, 0) + 1
69 if count <= max_repetitions:
70 cleaned.append(line)
71 seen[line_stripped] = count
72
73 return '\n'.join(cleaned).strip()
74
75# ==================== 🔍 استخراج نص من صورة ====================
76def extract_text_from_image(model, processor, image: Image.Image, prompt: str = None) -> str:
77 """استخراج النص من صورة واحدة باستخدام النموذج"""
78 if prompt is None:
79 prompt = "اقرأ النص في هذه الصورة كاملاً من البداية إلى النهاية."
80
81 # تحضير الصورة: ضبط الأبعاد لمضاعفات 64
82 w, h = image.size
83 new_w = ((w + 63) // 64) * 64
84 new_h = ((h + 63) // 64) * 64
85 if (new_w, new_h) != (w, h):
86 image = image.resize((new_w, new_h), Image.Resampling.LANCZOS)
87
88 messages = [{
89 "role": "user",
90 "content": [
91 {"type": "image", "image": image},
92 {"type": "text", "text": prompt}
93 ]
94 }]
95
96 text_input = processor.apply_chat_template(
97 messages, tokenize=False, add_generation_prompt=True
98 )
99 image_inputs, _ = process_vision_info(messages)
100
101 inputs = processor(
102 text=[text_input],
103 images=image_inputs,
104 padding=True,
105 return_tensors="pt"
106 ).to(DEVICE)
107
108 with torch.inference_mode():
109 generated_ids = model.generate(
110 **inputs,
111 max_new_tokens=2048,
112 do_sample=False,
113 repetition_penalty=1.2,
114 no_repeat_ngram_size=3,
115 pad_token_id=processor.tokenizer.pad_token_id,
116 eos_token_id=processor.tokenizer.eos_token_id,
117 )
118
119 input_len = inputs.input_ids.shape[1]
120 output_text = processor.batch_decode(
121 generated_ids[:, input_len:],
122 skip_special_tokens=True,
123 clean_up_tokenization_spaces=False
124 )[0]
125
126 return clean_output(output_text.strip())
127
128# ==================== 📄 معالجة ملف PDF كامل ====================
129def process_pdf(
130 pdf_path: str,
131 model,
132 processor,
133 output_path: str = None,
134 start_page: int = 0,
135 end_page: int = None,
136 dpi: int = 150,
137 prompt: str = None
138) -> dict:
139 """
140 معالجة ملف PDF كامل واستخراج النص من كل صفحة
141
142 Args:
143 pdf_path: مسار ملف الـ PDF
144 model: النموذج المُحمّل
145 processor: معالج النموذج
146 output_path: مسار ملف المخرجات (اختياري)
147 start_page: رقم الصفحة الأولى (0-مفهرس)
148 end_page: رقم الصفحة الأخيرة (None = حتى النهاية)
149 dpi: دقة تحويل الصفحة إلى صورة
150 prompt: البرومبت المستخدم للاستخراج
151
152 Returns:
153 dict: {
154 'total_pages': int,
155 'processed_pages': int,
156 'results': [ { 'page': int, 'text': str, 'time': float }, ... ],
157 'total_time': float
158 }
159 """
160 import fitz
161
162 doc = fitz.open(pdf_path)
163 total_pages = len(doc)
164
165 if end_page is None:
166 end_page = total_pages
167
168 results = []
169 total_start = time.time()
170
171 print(f"[INFO] Processing: {pdf_path}")
172 print(f"[INFO] Pages: {start_page+1} to {end_page} of {total_pages}")
173
174 for page_num in range(start_page, min(end_page, total_pages)):
175 page_start = time.time()
176
177 try:
178 # تحويل الصفحة إلى صورة
179 image = pdf_page_to_image(pdf_path, page_num, dpi=dpi)
180
181 # استخراج النص
182 text = extract_text_from_image(model, processor, image, prompt)
183
184 page_time = time.time() - page_start
185
186 results.append({
187 'page': page_num + 1, # صفحات مفهرسة من 1
188 'text': text,
189 'time': round(page_time, 2),
190 'image_size': image.size
191 })
192
193 print(f"[✓] Page {page_num+1}/{total_pages} | Time: {page_time:.2f}s | Chars: {len(text)}")
194
195 except Exception as e:
196 print(f"[✗] Page {page_num+1} Error: {str(e)}")
197 results.append({
198 'page': page_num + 1,
199 'text': f"[ERROR: {str(e)}]",
200 'time': 0,
201 'error': True
202 })
203
204 total_time = time.time() - total_start
205 doc.close()
206
207 # حفظ النتائج في ملف نصي إذا طُلب
208 if output_path:
209 save_results_to_file(results, output_path)
210 print(f"[INFO] Results saved to: {output_path}")
211
212 return {
213 'total_pages': total_pages,
214 'processed_pages': len(results),
215 'results': results,
216 'total_time': round(total_time, 2),
217 'avg_time_per_page': round(total_time / len(results), 2) if results else 0
218 }
219
220# ==================== 💾 حفظ النتائج ====================
221def save_results_to_file(results: list, output_path: str, format: str = 'txt'):
222 """حفظ نتائج الاستخراج في ملف"""
223 os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True)
224
225 if format == 'txt':
226 with open(output_path, 'w', encoding='utf-8') as f:
227 for item in results:
228 f.write(f"\n{'='*60}\n")
229 f.write(f"📄 الصفحة {item['page']}\n")
230 f.write(f"⏱️ الوقت: {item['time']} ثانية\n")
231 f.write(f"{'='*60}\n\n")
232 f.write(item['text'])
233 f.write("\n\n")
234
235 elif format == 'json':
236 import json
237 with open(output_path, 'w', encoding='utf-8') as f:
238 json.dump(results, f, ensure_ascii=False, indent=2)
239
240 elif format == 'md':
241 with open(output_path, 'w', encoding='utf-8') as f:
242 f.write("# 📄 نتائج استخراج النص من PDF\n\n")
243 for item in results:
244 f.write(f"## الصفحة {item['page']}\n")
245 f.write(f"- ⏱️ الوقت: {item['time']} ثانية\n")
246 f.write(f"- 📏 حجم الصورة: {item['image_size']}\n\n")
247 f.write("```text\n")
248 f.write(item['text'])
249 f.write("\n```\n\n")
250
251# ==================== 🚀 نقطة الدخول ====================
252if __name__ == "__main__":
253 import argparse
254
255 parser = argparse.ArgumentParser(description="📄 Arabic OCR for PDF using Qwen3.5-0.8B")
256 parser.add_argument('--pdf', type=str, required=True, help='مسار ملف الـ PDF')
257 parser.add_argument('--model', type=str, default='sherif1313/Arabic-Qwen3.5-OCR-v4', help='مسار النموذج')
258 parser.add_argument('--output', type=str, default=None, help='مسار ملف المخرجات')
259 parser.add_argument('--pages', type=str, default='all', help='الصفحات: all أو 1-5 أو 3')
260 parser.add_argument('--dpi', type=int, default=150, help='دقة التحويل (افتراضي: 150)')
261 parser.add_argument('--prompt', type=str, default=None, help='برومبت مخصص')
262 parser.add_argument('--format', type=str, default='txt', choices=['txt', 'json', 'md'], help='تنسيق المخرجات')
263
264 args = parser.parse_args()
265
266 # تحليل نطاق الصفحات
267 if args.pages == 'all':
268 start_page, end_page = 0, None
269 elif '-' in args.pages:
270 parts = args.pages.split('-')
271 start_page = int(parts[0]) - 1
272 end_page = int(parts[1]) if len(parts) > 1 and parts[1] else None
273 else:
274 page = int(args.pages) - 1
275 start_page, end_page = page, page + 1
276
277 # تحميل النموذج
278 model, processor = load_model(args.model)
279
280 # معالجة الـ PDF
281 results = process_pdf(
282 pdf_path=args.pdf,
283 model=model,
284 processor=processor,
285 output_path=args.output,
286 start_page=start_page,
287 end_page=end_page,
288 dpi=args.dpi,
289 prompt=args.prompt
290 )
291
292 # طباعة ملخص
293 print(f"\n{'='*60}")
294 print("📊 ملخص المعالجة")
295 print(f"{'='*60}")
296 print(f"📄 إجمالي الصفحات: {results['total_pages']}")
297 print(f"✅ الصفحات المُعالَجة: {results['processed_pages']}")
298 print(f"⏱️ الوقت الكلي: {results['total_time']} ثانية")
299 print(f"⚡ متوسط الوقت/صفحة: {results['avg_time_per_page']} ثانية")
300 print(f"{'='*60}")