| |
| |
| """ |
| 🤖 Arabic OCR - Hugging Face Spaces Version |
| Model: Qwen3.5-0.8B-VL with LoRA |
| No Quantization - Full Precision |
| """ |
|
|
| import os |
| import time |
| import torch |
| from PIL import Image |
| import gradio as gr |
| from transformers import AutoProcessor, Qwen3_5ForConditionalGeneration |
| from qwen_vl_utils import process_vision_info |
|
|
| |
| if torch.cuda.is_available(): |
| device = "cuda" |
| dtype = torch.float16 |
| print(f"✅ Using GPU: {torch.cuda.get_device_name(0)}") |
| elif torch.backends.mps.is_available(): |
| device = "mps" |
| dtype = torch.float16 |
| print("✅ Using Apple Silicon (MPS)") |
| else: |
| device = "cpu" |
| dtype = torch.float32 |
| print("⚠️ Using CPU (slower inference)") |
|
|
| print(f"[INFO] Device: {device} | Dtype: {dtype}") |
|
|
| |
| def load_model(): |
| """تحميل النموذج والمعالج مع إدارة الذاكرة""" |
| model_path = os.getenv("MODEL_PATH", "sherif1313/Arabic-Qwen3.5-OCR-v4") |
| |
| print(f"[INFO] Loading model from: {model_path}") |
| |
| processor = AutoProcessor.from_pretrained(model_path, trust_remote_code=True) |
| |
| model = Qwen3_5ForConditionalGeneration.from_pretrained( |
| model_path, |
| torch_dtype=dtype, |
| device_map="auto" if device == "cuda" else None, |
| trust_remote_code=True, |
| low_cpu_mem_usage=True, |
| ) |
| |
| model.eval() |
| print("[INFO] Model loaded successfully!") |
| return model, processor |
|
|
| |
| try: |
| model, processor = load_model() |
| except Exception as e: |
| print(f"[ERROR] Failed to load model: {e}") |
| model = None |
| processor = None |
|
|
| |
| def prepare_image(image: Image.Image, max_size: int = 768) -> Image.Image: |
| """تحضير الصورة: ضغط + ضبط الأبعاد لمضاعفات 64""" |
| if max(image.size) > max_size: |
| image.thumbnail((max_size, max_size), Image.Resampling.LANCZOS) |
| |
| w, h = image.size |
| new_w = ((w + 63) // 64) * 64 |
| new_h = ((h + 63) // 64) * 64 |
| if (new_w, new_h) != image.size: |
| image = image.resize((new_w, new_h), Image.Resampling.LANCZOS) |
| |
| return image |
|
|
| def clean_output(text: str, max_repetitions: int = 2) -> str: |
| """تنظيف التكرار في المخرجات""" |
| if not text: |
| return text |
| |
| import re |
| text = re.sub(r'(.)\1{4,}', r'\1\1\1', text) |
| |
| lines = text.strip().split('\n') |
| cleaned = [] |
| seen = {} |
| for line in lines: |
| line_stripped = line.strip() |
| if not line_stripped: |
| continue |
| count = seen.get(line_stripped, 0) + 1 |
| if count <= max_repetitions: |
| cleaned.append(line) |
| seen[line_stripped] = count |
| |
| return '\n'.join(cleaned).strip() |
|
|
| |
| def extract_text(image, prompt: str = None) -> tuple[str, str]: |
| """استخراج النص من الصورة""" |
| if model is None or processor is None: |
| return "❌ Error: Model not loaded", "0.00" |
| |
| if image is None: |
| return "⚠️ Please upload an image", "0.00" |
| |
| start_time = time.time() |
| |
| try: |
| if isinstance(image, str): |
| image_pil = Image.open(image).convert("RGB") |
| elif isinstance(image, Image.Image): |
| image_pil = image.convert("RGB") |
| else: |
| image_pil = Image.fromarray(image).convert("RGB") |
| |
| image_pil = prepare_image(image_pil) |
| |
| if prompt is None or not prompt.strip(): |
| prompt = "اقرأ النص في هذه الصورة كاملاً من البداية إلى النهاية." |
| |
| messages = [{ |
| "role": "user", |
| "content": [ |
| {"type": "image", "image": image_pil}, |
| {"type": "text", "text": prompt} |
| ] |
| }] |
| |
| text_input = processor.apply_chat_template( |
| messages, tokenize=False, add_generation_prompt=True |
| ) |
| image_inputs, _ = process_vision_info(messages) |
| |
| inputs = processor( |
| text=[text_input], |
| images=image_inputs, |
| padding=True, |
| return_tensors="pt" |
| ).to(device) |
| |
| with torch.inference_mode(): |
| generated_ids = model.generate( |
| **inputs, |
| max_new_tokens=512, |
| do_sample=False, |
| temperature=1.0, |
| repetition_penalty=1.2, |
| no_repeat_ngram_size=3, |
| pad_token_id=processor.tokenizer.pad_token_id, |
| eos_token_id=processor.tokenizer.eos_token_id, |
| ) |
| |
| input_len = inputs.input_ids.shape[1] |
| output_text = processor.batch_decode( |
| generated_ids[:, input_len:], |
| skip_special_tokens=True, |
| clean_up_tokenization_spaces=False |
| )[0] |
| |
| output_text = clean_output(output_text.strip()) |
| |
| elapsed = time.time() - start_time |
| |
| return output_text, f"{elapsed:.2f} seconds" |
| |
| except torch.cuda.OutOfMemoryError: |
| torch.cuda.empty_cache() |
| return "❌ Out of Memory. Try a smaller image.", "0.00" |
| except Exception as e: |
| print(f"[ERROR] {e}") |
| import traceback |
| traceback.print_exc() |
| return f"❌ Error: {str(e)}", "0.00" |
|
|
| |
| def create_interface(): |
| """إنشاء واجهة المستخدم""" |
| |
| with gr.Blocks( |
| title="Arabic OCR - Qwen3.5-0.8B", |
| theme=gr.themes.Soft(), |
| css=""" |
| .header { text-align: center; margin-bottom: 20px; } |
| .output-box { min-height: 200px; } |
| """ |
| ) as demo: |
| |
| gr.Markdown(""" |
| # 📝 Arabic Handwritten & Printed OCR V4 |
| ### Powered by Qwen3.5-0.8B |
| |
| Upload an image containing Arabic text, and the model will extract it. |
| |
| ✨ **Features:** |
| - 🌍 Arabic support |
| - ✍️ Handwritten & printed text |
| - 🔤 Preserves diacritics (تشكيل) |
| - ⚡ Full precision (no quantization) |
| """, elem_classes="header") |
| |
| with gr.Row(): |
| with gr.Column(scale=1): |
| |
| image_input = gr.Image( |
| label="📷 Upload Image", |
| type="pil", |
| height=300, |
| sources=["upload", "clipboard"] |
| ) |
| |
| prompt_input = gr.Textbox( |
| label="📝 Custom Prompt (Optional)", |
| placeholder="اقرأ النص في هذه الصورة...", |
| value="اقرأ النص في هذه الصورة كاملاً من البداية إلى النهاية.", |
| lines=2 |
| ) |
| |
| submit_btn = gr.Button( |
| "🔍 Extract Text", |
| variant="primary", |
| size="lg" |
| ) |
| |
| |
| |
| |
| gr.Examples( |
| label="📋 Examples (Optional)", |
| examples=[ |
| ["https://huggingface.co/sherif1313/Arabic-Qwen3.5-OCR-v4/blob/main/00009.png"], |
| ["https://huggingface.co/sherif1313/Arabic-Qwen3.5-OCR-v4/blob/main/00013.png"], |
| ["https://huggingface.co/sherif1313/Arabic-Qwen3.5-OCR-v4/blob/main/00014.png"], |
| ["https://huggingface.co/sherif1313/Arabic-Qwen3.5-OCR-v4/blob/main/00029.png"], |
| ["https://huggingface.co/sherif1313/Arabic-Qwen3.5-OCR-v4/blob/main/00241.png"], |
| ["https://huggingface.co/sherif1313/Arabic-Qwen3.5-OCR-v4/blob/main/00310.png"], |
| ], |
| inputs=[image_input], |
| cache_examples=False |
| ) |
| |
| with gr.Column(scale=1): |
| output_text = gr.Textbox( |
| label="📄 Extracted Text", |
| lines=12, |
| show_copy_button=True, |
| elem_classes="output-box" |
| ) |
| |
| time_output = gr.Textbox( |
| label="⏱️ Inference Time", |
| interactive=False, |
| value="-" |
| ) |
| |
| clear_btn = gr.Button("🗑️ Clear", variant="secondary") |
| |
| |
| submit_btn.click( |
| fn=extract_text, |
| inputs=[image_input, prompt_input], |
| outputs=[output_text, time_output] |
| ) |
| |
| clear_btn.click( |
| fn=lambda: (None, "", "-"), |
| inputs=[], |
| outputs=[image_input, prompt_input, time_output] |
| ) |
| |
| gr.Markdown(""" |
| ### 💡 Tips for Best Results: |
| 1. Use clear, well-lit images |
| 2. Crop to the text region if possible |
| 3. For handwritten text, ensure good contrast |
| 4. Custom prompts can improve accuracy for specific formats |
| """) |
| |
| return demo |
|
|
| |
| if __name__ == "__main__": |
| print("[INFO] Creating Gradio interface...") |
| |
| demo = create_interface() |
| |
| |
| demo.launch( |
| server_name="0.0.0.0", |
| server_port=int(os.getenv("PORT", 7860)), |
| share=False, |
| debug=os.getenv("DEBUG", "false").lower() == "true", |
| show_error=True |
| ) |
|
|