Files changed (1) hide show
  1. main (1).py +215 -0
main (1).py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Query
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from typing import List, Dict
4
+ from io import BytesIO
5
+ from PIL import Image
6
+ import uvicorn
7
+ import os
8
+ import numpy as np
9
+ import cv2
10
+ import re
11
+
12
+ # PDF support
13
+ try:
14
+ from pdf2image import convert_from_bytes
15
+ PDF_AVAILABLE = True
16
+ except:
17
+ PDF_AVAILABLE = False
18
+
19
+ # Models
20
+ paddle_detector = None
21
+ paddle_recognizer = None
22
+
23
+ app = FastAPI(title="OCR Scan Vision API", version="1.0.0")
24
+
25
+ app.add_middleware(
26
+ CORSMiddleware,
27
+ allow_origins=["*"],
28
+ allow_credentials=True,
29
+ allow_methods=["*"],
30
+ allow_headers=["*"],
31
+ )
32
+
33
+ # -------------------- تنظيف النص العربي --------------------
34
+ def clean_arabic_text(text: str) -> str:
35
+ """
36
+ - يحافظ على الكلمات العربية والأرقام
37
+ - يحول الرموز المهمة (: - / _) لمسافة
38
+ - يشيل باقي الرموز
39
+ - يزيل التشكيل
40
+ - يضبط المسافات
41
+ """
42
+ if not text:
43
+ return ""
44
+
45
+ # الرموز المهمة تتحول لمسافة
46
+ text = re.sub(r"[:\-_/]", " ", text)
47
+
48
+ # شيل باقي الرموز
49
+ text = re.sub(r"[^\u0600-\u06FF0-9\s]", "", text)
50
+
51
+ # إزالة التشكيل
52
+ text = re.sub(r"[\u064B-\u065F]", "", text)
53
+
54
+ # إزالة مسافات زيادة
55
+ text = re.sub(r"\s+", " ", text)
56
+
57
+ return text.strip()
58
+
59
+
60
+ def get_models():
61
+ global paddle_detector, paddle_recognizer
62
+
63
+ if paddle_detector is None or paddle_recognizer is None:
64
+ try:
65
+ from paddlex import create_model
66
+ print("Loading PaddleX OCR models...")
67
+ paddle_detector = create_model("PP-OCRv5_server_det")
68
+ paddle_recognizer = create_model("arabic_PP-OCRv5_mobile_rec")
69
+ print("Models loaded.")
70
+ except Exception as e:
71
+ raise HTTPException(
72
+ status_code=500,
73
+ detail=f"OCR models failed to load: {str(e)}"
74
+ )
75
+
76
+ return paddle_detector, paddle_recognizer
77
+
78
+
79
+ def process_image(img: np.ndarray, detector, recognizer, min_conf: float) -> List[Dict]:
80
+ h_img, w_img = img.shape[:2]
81
+
82
+ # 1️⃣ كشف النصوص
83
+ results = detector.predict(img)
84
+
85
+ all_rois = []
86
+ all_bboxes = []
87
+
88
+ for result in results:
89
+ boxes = result.get("dt_polys", [])
90
+ for box in boxes:
91
+ pts = np.array(box, dtype=np.int32)
92
+ x, y, w, h = cv2.boundingRect(pts)
93
+
94
+ x1 = max(x, 0)
95
+ y1 = max(y, 0)
96
+ x2 = min(x + w, w_img)
97
+ y2 = min(y + h, h_img)
98
+
99
+ if x2 > x1 and y2 > y1:
100
+ roi = img[y1:y2, x1:x2]
101
+ if roi.size > 0:
102
+ all_rois.append(roi)
103
+ all_bboxes.append([x1, y1, x2, y2])
104
+
105
+ # 2️⃣ التعرف على النصوص
106
+ ocr_results = []
107
+
108
+ for i, roi in enumerate(all_rois):
109
+ try:
110
+ rec_gen = recognizer.predict(roi)
111
+ rec = next(rec_gen)
112
+ raw_text = rec.get("rec_text", "")
113
+ score = float(rec.get("rec_score", 0.0))
114
+ text = clean_arabic_text(raw_text)
115
+ except:
116
+ text = ""
117
+ score = 0.0
118
+
119
+ if score >= min_conf and text:
120
+ ocr_results.append({
121
+ "box_id": i + 1,
122
+ "text": text,
123
+ "confidence": round(score, 4),
124
+ "bbox": all_bboxes[i]
125
+ })
126
+
127
+ # ✅ ترتيب عربي: فوق → تحت ، يمين → شمال
128
+ ocr_results.sort(
129
+ key=lambda x: (
130
+ x["bbox"][1], # Y
131
+ -x["bbox"][0] # X (RTL)
132
+ )
133
+ )
134
+
135
+ return ocr_results
136
+
137
+
138
+ @app.get("/")
139
+ def root():
140
+ return {"name": "OCR Scan Vision API", "status": "ok", "pdf_support": PDF_AVAILABLE}
141
+
142
+
143
+ @app.get("/health")
144
+ def health():
145
+ return {"status": "healthy"}
146
+
147
+
148
+ @app.post("/ocr")
149
+ async def ocr_image(
150
+ file: UploadFile = File(...),
151
+ min_conf: float = Query(default=0.0, ge=0.0, le=1.0),
152
+ ):
153
+ try:
154
+ contents = await file.read()
155
+ pil_img = Image.open(BytesIO(contents)).convert("RGB")
156
+ img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
157
+ except:
158
+ raise HTTPException(status_code=400, detail="Invalid image file")
159
+
160
+ detector, recognizer = get_models()
161
+ ocr_results = process_image(img, detector, recognizer, min_conf)
162
+
163
+ full_text = "\n".join([r["text"] for r in ocr_results])
164
+
165
+ return {
166
+ "items": ocr_results,
167
+ "text": full_text,
168
+ "total_boxes": len(ocr_results)
169
+ }
170
+
171
+
172
+ @app.post("/ocr-pdf")
173
+ async def ocr_pdf(
174
+ file: UploadFile = File(...),
175
+ dpi: int = Query(default=300, ge=72, le=600),
176
+ min_conf: float = Query(default=0.0, ge=0.0, le=1.0),
177
+ ):
178
+ if not PDF_AVAILABLE:
179
+ raise HTTPException(status_code=500, detail="PDF support not available")
180
+
181
+ try:
182
+ contents = await file.read()
183
+ pages = convert_from_bytes(contents, dpi=dpi)
184
+ except Exception as e:
185
+ raise HTTPException(status_code=400, detail=f"Invalid PDF file: {e}")
186
+
187
+ detector, recognizer = get_models()
188
+
189
+ all_results = []
190
+ all_text = []
191
+
192
+ for page_num, pil_img in enumerate(pages, start=1):
193
+ img = cv2.cvtColor(np.array(pil_img.convert("RGB")), cv2.COLOR_RGB2BGR)
194
+ page_results = process_image(img, detector, recognizer, min_conf)
195
+
196
+ for item in page_results:
197
+ item["page"] = page_num
198
+
199
+ all_results.extend(page_results)
200
+
201
+ page_text = "\n".join([r["text"] for r in page_results])
202
+ if page_text:
203
+ all_text.append(f"--- Page {page_num} ---\n{page_text}")
204
+
205
+ return {
206
+ "pages": len(pages),
207
+ "items": all_results,
208
+ "text": "\n\n".join(all_text),
209
+ "total_boxes": len(all_results)
210
+ }
211
+
212
+
213
+ if __name__ == "__main__":
214
+ port = int(os.environ.get("PORT", 7860))
215
+ uvicorn.run("main:app", host="0.0.0.0", port=port)