CoolFace
Apppublic

Fahad0saad/arabic-ocr-v2

sourceHugging Facemitupdated 3mo agoView on Hugging Face
0likes
ocr_engine.py164 linesDownload Raw Back to app
1"""Qwen3-VL OCR integration via OpenRouter."""2from __future__ import annotations3 4import base645import os6import re7from pathlib import Path8from typing import Optional9 10import httpx11 12OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"13 14DEFAULT_MODEL = "qwen/qwen3-vl-8b-instruct"15FALLBACK_MODEL = "qwen/qwen3-vl-235b-a22b-instruct"16 17OCR_PROMPT = (18    "أنت نظام متخصص في التعرف الضوئي على الحروف (OCR) للنصوص العربية. "19    "قم باستخراج كل النص العربي الظاهر في الصورة بدقة عالية، مع الحفاظ على:\n"20    "1. ترتيب الفقرات والأسطر كما تظهر في الصورة\n"21    "2. علامات الترقيم والمسافات\n"22    "3. الأرقام والرموز كما هي\n\n"23    "أعد فقط النص المستخرج بدون أي شرح أو مقدمة أو تعليق. "24    "إذا لم تجد نصاً عربياً، أعد النص بأي لغة موجود في الصورة."25)26 27TASHKEEL_PROMPT = (28    "أضف التشكيل الكامل (الحركات) إلى النص العربي التالي بشكل صحيح نحوياً وصرفياً، "29    "مع الحفاظ على المعنى الأصلي. أعد فقط النص المشكّل بدون أي تعليق:\n\n"30)31 32CORRECTION_PROMPT = (33    "صحّح الأخطاء الإملائية والنحوية في النص العربي التالي الناتج عن OCR، "34    "مع الحفاظ على المعنى الأصلي وترتيب الأسطر. أعد فقط النص المصحّح بدون تعليق:\n\n"35)36 37 38def _api_key() -> str:39    key = os.environ.get("OPENROUTER_API_KEY", "").strip()40    if not key:41        raise RuntimeError("OPENROUTER_API_KEY environment variable is not set")42    return key43 44 45def _image_to_data_url(image_path: str | Path) -> str:46    p = Path(image_path)47    ext = p.suffix.lower().lstrip(".") or "png"48    mime = "jpeg" if ext in ("jpg", "jpeg") else ext49    data = base64.b64encode(p.read_bytes()).decode("ascii")50    return f"data:image/{mime};base64,{data}"51 52 53async def _call_model(54    model: str, messages: list[dict], timeout: float = 120.055) -> str:56    headers = {57        "Authorization": f"Bearer {_api_key()}",58        "Content-Type": "application/json",59        # OpenRouter ranking headers60        "HTTP-Referer": os.environ.get("OPENROUTER_REFERER", "https://arabic-ocr.local"),61        "X-Title": "Arabic OCR v2",62    }63    payload = {64        "model": model,65        "messages": messages,66        "temperature": 0.1,67        "max_tokens": 4096,68    }69    async with httpx.AsyncClient(timeout=timeout) as client:70        resp = await client.post(OPENROUTER_URL, headers=headers, json=payload)71        resp.raise_for_status()72        data = resp.json()73 74    choices = data.get("choices") or []75    if not choices:76        return ""77    msg = choices[0].get("message", {})78    content = msg.get("content")79    if isinstance(content, list):80        # Some vision models return list of parts81        parts = []82        for part in content:83            if isinstance(part, dict) and part.get("type") == "text":84                parts.append(part.get("text", ""))85            elif isinstance(part, str):86                parts.append(part)87        return "".join(parts).strip()88    return (content or "").strip()89 90 91def _strip_fences(text: str) -> str:92    text = text.strip()93    if text.startswith("```"):94        text = re.sub(r"^```[a-zA-Z]*\n?", "", text)95        text = re.sub(r"\n?```$", "", text)96    return text.strip()97 98 99async def run_ocr(100    image_path: str | Path,101    model: str = DEFAULT_MODEL,102    fallback: str = FALLBACK_MODEL,103) -> str:104    """Run vision OCR on a single image, with fallback to a stronger model."""105    data_url = _image_to_data_url(image_path)106    messages = [107        {108            "role": "user",109            "content": [110                {"type": "text", "text": OCR_PROMPT},111                {"type": "image_url", "image_url": {"url": data_url}},112            ],113        }114    ]115 116    try:117        out = await _call_model(model, messages)118        text = _strip_fences(out)119        if text:120            return text121    except Exception:122        pass123 124    # Fallback model125    out = await _call_model(fallback, messages)126    return _strip_fences(out)127 128 129async def post_process(130    text: str,131    auto_tashkeel: bool = False,132    ai_correction: bool = False,133    model: str = DEFAULT_MODEL,134) -> str:135    """Apply optional AI post-processing: correction then tashkeel."""136    if not text.strip():137        return text138 139    if ai_correction:140        try:141            corrected = await _call_model(142                model,143                [{"role": "user", "content": CORRECTION_PROMPT + text}],144            )145            corrected = _strip_fences(corrected)146            if corrected:147                text = corrected148        except Exception:149            pass150 151    if auto_tashkeel:152        try:153            tashkeel = await _call_model(154                model,155                [{"role": "user", "content": TASHKEEL_PROMPT + text}],156            )157            tashkeel = _strip_fences(tashkeel)158            if tashkeel:159                text = tashkeel160        except Exception:161            pass162 163    return text164