sherif1313 commited on
Commit
4caf806
·
verified ·
1 Parent(s): e856622

Upload app.py

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