sharshar1 commited on
Commit
699052d
·
verified ·
1 Parent(s): 4860b6a

Upload main.py

Browse files
Files changed (1) hide show
  1. main.py +76 -67
main.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from fastapi import FastAPI, File, UploadFile, HTTPException, Query
2
  from fastapi.middleware.cors import CORSMiddleware
3
  from typing import List, Dict, Any
@@ -19,7 +20,11 @@ except ImportError:
19
  # استيراد مكتبة PaddleOCR الرسمية
20
  from paddleocr import PaddleOCR
21
 
22
- app = FastAPI(title="OCR Scan Vision API", version="2.0.0")
 
 
 
 
23
 
24
  app.add_middleware(
25
  CORSMiddleware,
@@ -29,46 +34,54 @@ app.add_middleware(
29
  allow_headers=["*"],
30
  )
31
 
32
- # متغير عام لتخزين الموديل في الذاكرة
33
  ocr_model = None
34
 
35
- # -------------------- تنظيف النص العربي --------------------
36
- def clean_arabic_text(text: str) -> str:
37
  """
38
- تنظيف النص مع الحفاظ على الأرقام والعربية
 
 
 
39
  """
40
  if not text:
41
  return ""
42
 
43
- # استبدال الرموز بمسافات
44
- text = re.sub(r"[:\-_/]", " ", text)
45
- # الاحتفاظ بالعربية والأرقام والمسافات فقط
46
- text = re.sub(r"[^\u0600-\u06FF0-9\s]", "", text)
47
- # إزالة التشكيل
48
- text = re.sub(r"[\u064B-\u065F]", "", text)
49
- # ضبط المسافات المتكررة
50
- text = re.sub(r"\s+", " ", text)
 
51
 
52
- return text.strip()
 
 
 
53
 
54
  def get_model():
55
- """
56
- تحميل الموديل مرة واحدة عند بدء التشغيل أو أول طلب.
57
- """
58
  global ocr_model
59
  if ocr_model is None:
60
- print("Loading PaddleOCR model (Arabic)...")
61
- # تم تفعيل use_angle_cls=True هنا، وهذا يكفي لتشغيل المصحح
62
- ocr_model = PaddleOCR(use_angle_cls=True, lang='ar')
63
- print("Model loaded successfully.")
 
 
 
 
 
64
  return ocr_model
65
 
66
- def organize_boxes(results: List, y_threshold: int = 15) -> List[Dict]:
67
  """
68
- خوارزمية لترتيب النتائج:
69
- 1. تجميع الكلمات التي تقع على نفس السطر تقريباً.
70
- 2. ترتيب الأسطر من الأعلى للأسفل.
71
- 3. ترتيب الكلمات داخل السطر الواحد من اليمين لليسار.
72
  """
73
  if not results:
74
  return []
@@ -79,13 +92,11 @@ def organize_boxes(results: List, y_threshold: int = 15) -> List[Dict]:
79
  coords = item[0]
80
  text_data = item[1]
81
 
82
- # حساب إحداثيات الصندوق
83
  y_min = min(c[1] for c in coords)
84
  y_max = max(c[1] for c in coords)
85
  x_min = min(c[0] for c in coords)
86
  x_max = max(c[0] for c in coords)
87
 
88
- # مركز الصندوق رأسياً
89
  y_center = (y_min + y_max) / 2
90
 
91
  boxes.append({
@@ -93,17 +104,17 @@ def organize_boxes(results: List, y_threshold: int = 15) -> List[Dict]:
93
  "confidence": text_data[1],
94
  "coords": coords,
95
  "y_center": y_center,
96
- "x_max": x_max, # نستخدم أقصى اليمين للترتيب العربي
97
  "bbox": [int(x_min), int(y_min), int(x_max), int(y_max)]
98
  })
99
 
100
- # ترتيب أولي حسب الموقع الرأسي (Y)
101
  boxes.sort(key=lambda b: b["y_center"])
102
 
103
  lines = []
104
  current_line = []
105
 
106
- # تجميع العناصر في أسطر
107
  for box in boxes:
108
  if not current_line:
109
  current_line.append(box)
@@ -111,63 +122,66 @@ def organize_boxes(results: List, y_threshold: int = 15) -> List[Dict]:
111
 
112
  last_box = current_line[-1]
113
 
114
- # إذا كان الفرق في الارتفاع الرأسي بسيطاً، نعتبرهم في نفس السطر
115
  if abs(box["y_center"] - last_box["y_center"]) < y_threshold:
116
  current_line.append(box)
117
  else:
118
- # بدء سطر جديد
119
  lines.append(current_line)
120
  current_line = [box]
121
 
122
  if current_line:
123
  lines.append(current_line)
124
 
125
- # الترتيب النهائي
126
  final_sorted_boxes = []
127
  for line in lines:
128
- # نرتب عناصر السطر الواحد تنازلياً حسب X (من اليمين لليسار)
129
  line.sort(key=lambda b: -b["x_max"])
130
  final_sorted_boxes.extend(line)
131
 
132
  return final_sorted_boxes
133
 
134
  def process_image_full(img: np.ndarray, model, min_conf: float) -> List[Dict]:
135
- # PaddleOCR يستقبل الصورة مباشرة
 
 
136
  result = model.ocr(img)
137
 
138
- # التحقق من أن النتيجة صالحة وليست فارغة
139
- # PaddleOCR returns [ [line1, line2, ...] ] for single image
140
  if not result or result[0] is None:
 
141
  return []
142
 
143
  raw_results = result[0]
144
 
145
- # تصفية النتائج بطريقة آمنة لتجنب الأخطاء (IndexError)
146
  filtered_results = []
147
  for item in raw_results:
148
- # التحقق من هيكل العنصر: يجب أن يكون قائمة تحتوي على عنصرين [coords, (text, conf)]
149
  if not isinstance(item, (list, tuple)) or len(item) < 2:
150
  continue
151
-
152
  text_part = item[1]
153
-
154
- # التحقق من جزء النص والثقة: يجب أن يكون (text, conf)
155
  if isinstance(text_part, (list, tuple)) and len(text_part) >= 2:
156
  text, conf = text_part
 
157
  if conf >= min_conf:
158
  filtered_results.append(item)
159
-
160
- # تطبيق خوارزمية الترتيب والفلترة
161
- sorted_boxes = organize_boxes(filtered_results)
162
 
163
  final_output = []
164
  for i, box in enumerate(sorted_boxes):
165
- clean_text = clean_arabic_text(box["text"])
166
- if clean_text:
 
 
 
 
 
 
167
  final_output.append({
168
  "box_id": i + 1,
169
- "text": clean_text,
170
- "original_text": box["text"],
171
  "confidence": round(box["confidence"], 4),
172
  "bbox": box["bbox"]
173
  })
@@ -176,16 +190,12 @@ def process_image_full(img: np.ndarray, model, min_conf: float) -> List[Dict]:
176
 
177
  @app.get("/")
178
  def root():
179
- return {"name": "OCR Scan Vision API v2", "status": "ok", "pdf_support": PDF_AVAILABLE}
180
-
181
- @app.get("/health")
182
- def health():
183
- return {"status": "healthy"}
184
 
185
  @app.post("/ocr")
186
  async def ocr_image(
187
  file: UploadFile = File(...),
188
- min_conf: float = Query(default=0.6, ge=0.0, le=1.0),
189
  ):
190
  try:
191
  contents = await file.read()
@@ -197,29 +207,29 @@ async def ocr_image(
197
  model = get_model()
198
  ocr_results = process_image_full(img, model, min_conf)
199
 
200
- # دمج النصوص بأسطر جديدة
201
  full_text = "\n".join([r["text"] for r in ocr_results])
202
 
203
  return {
204
  "items": ocr_results,
205
  "text": full_text,
206
- "total_boxes": len(ocr_results)
207
  }
208
 
209
  @app.post("/ocr-pdf")
210
  async def ocr_pdf(
211
  file: UploadFile = File(...),
212
  dpi: int = Query(default=300, ge=72, le=600),
213
- min_conf: float = Query(default=0.6, ge=0.0, le=1.0),
214
  ):
215
  if not PDF_AVAILABLE:
216
- raise HTTPException(status_code=500, detail="PDF support not available")
217
 
218
  try:
219
  contents = await file.read()
220
  pages = convert_from_bytes(contents, dpi=dpi)
221
  except Exception as e:
222
- raise HTTPException(status_code=400, detail=f"Invalid PDF file: {e}")
223
 
224
  model = get_model()
225
  all_results = []
@@ -227,23 +237,22 @@ async def ocr_pdf(
227
 
228
  for page_num, pil_img in enumerate(pages, start=1):
229
  img = cv2.cvtColor(np.array(pil_img.convert("RGB")), cv2.COLOR_RGB2BGR)
230
-
231
  page_results = process_image_full(img, model, min_conf)
232
 
233
  for item in page_results:
234
  item["page"] = page_num
235
 
236
  all_results.extend(page_results)
237
-
238
- page_text = "\n".join([r["text"] for r in page_results])
239
- if page_text:
240
- all_text.append(f"--- Page {page_num} ---\n{page_text}")
241
 
242
  return {
243
  "pages": len(pages),
244
  "items": all_results,
245
  "text": "\n\n".join(all_text),
246
- "total_boxes": len(all_results)
247
  }
248
 
249
  if __name__ == "__main__":
 
1
+ import logging
2
  from fastapi import FastAPI, File, UploadFile, HTTPException, Query
3
  from fastapi.middleware.cors import CORSMiddleware
4
  from typing import List, Dict, Any
 
20
  # استيراد مكتبة PaddleOCR الرسمية
21
  from paddleocr import PaddleOCR
22
 
23
+ # إعداد الـ Logging
24
+ logging.basicConfig(level=logging.INFO)
25
+ logger = logging.getLogger("OCR_Contracts")
26
+
27
+ app = FastAPI(title="OCR Arabic Contracts API", version="3.0.0")
28
 
29
  app.add_middleware(
30
  CORSMiddleware,
 
34
  allow_headers=["*"],
35
  )
36
 
37
+ # متغير عام لتخزين الموديل
38
  ocr_model = None
39
 
40
+ # -------------------- تنظيف النص (مخصص للعقود) --------------------
41
+ def clean_contract_text(text: str) -> str:
42
  """
43
+ تنظيف مخصص للعقود العربية:
44
+ - يحافظ على العربية والأرقام
45
+ - يحافظ على رموز التواريخ والعقود (/, -, :, ., ())
46
+ - يزيل الرموز الغريبة فقط
47
  """
48
  if not text:
49
  return ""
50
 
51
+ # 1. إزالة أي رمز غير مرغوب فيه، لكن نترك:
52
+ # \u0600-\u06FF : الحروف العربية
53
+ # 0-9 : الأرقام
54
+ # \s : المسافات
55
+ # . , - / : ( ) [ ] _ : علامات الترقيم المهمة في العقود والتواريخ
56
+ clean = re.sub(r"[^\u0600-\u06FF0-9\s\.\,\-\/\:\(\)\[\]\_]", " ", text)
57
+
58
+ # 2. إزالة التشكيل (عادة غير مطلوب في العقود القانونية لتسهيل البحث)
59
+ clean = re.sub(r"[\u064B-\u065F]", "", clean)
60
 
61
+ # 3. ضبط المسافات المتكررة الناتجة عن الحذف
62
+ clean = re.sub(r"\s+", " ", clean)
63
+
64
+ return clean.strip()
65
 
66
  def get_model():
 
 
 
67
  global ocr_model
68
  if ocr_model is None:
69
+ logger.info("Loading PaddleOCR model for Arabic Contracts...")
70
+ try:
71
+ # lang='ar' يدعم العربية والإنجليزية (للأرقام)
72
+ # use_angle_cls=True ضروري للمستندات المائلة (Scanned Docs)
73
+ ocr_model = PaddleOCR(use_angle_cls=True, lang='ar')
74
+ logger.info("Model loaded successfully.")
75
+ except Exception as e:
76
+ logger.error(f"Failed to load model: {e}")
77
+ raise e
78
  return ocr_model
79
 
80
+ def organize_boxes_rtl(results: List, y_threshold: int = 15) -> List[Dict]:
81
  """
82
+ خوارزمية ترتيب النصوص العربية (RTL):
83
+ 1. تجميع الكلمات في أسطر بناءً على المحور Y.
84
+ 2. ترتيب الكلمات داخل كل سطر من اليمين إلى اليسار (X تنازلي).
 
85
  """
86
  if not results:
87
  return []
 
92
  coords = item[0]
93
  text_data = item[1]
94
 
 
95
  y_min = min(c[1] for c in coords)
96
  y_max = max(c[1] for c in coords)
97
  x_min = min(c[0] for c in coords)
98
  x_max = max(c[0] for c in coords)
99
 
 
100
  y_center = (y_min + y_max) / 2
101
 
102
  boxes.append({
 
104
  "confidence": text_data[1],
105
  "coords": coords,
106
  "y_center": y_center,
107
+ "x_max": x_max, # نستخدم أقصى اليمين للترتيب
108
  "bbox": [int(x_min), int(y_min), int(x_max), int(y_max)]
109
  })
110
 
111
+ # 1. ترتيب الصناديق رأسياً (من أعلى لأسفل)
112
  boxes.sort(key=lambda b: b["y_center"])
113
 
114
  lines = []
115
  current_line = []
116
 
117
+ # 2. تجميع الأسطر
118
  for box in boxes:
119
  if not current_line:
120
  current_line.append(box)
 
122
 
123
  last_box = current_line[-1]
124
 
125
+ # إذا كان الفرق في الارتفاع بسيط، نعتبرهم نفس السطر
126
  if abs(box["y_center"] - last_box["y_center"]) < y_threshold:
127
  current_line.append(box)
128
  else:
 
129
  lines.append(current_line)
130
  current_line = [box]
131
 
132
  if current_line:
133
  lines.append(current_line)
134
 
135
+ # 3. ترتيب كل سطر من اليمين لليسار (هام جداً للعربي)
136
  final_sorted_boxes = []
137
  for line in lines:
138
+ # ترتيب تنازلي حسب x_max (الأكبر هو الموجود في اليمين)
139
  line.sort(key=lambda b: -b["x_max"])
140
  final_sorted_boxes.extend(line)
141
 
142
  return final_sorted_boxes
143
 
144
  def process_image_full(img: np.ndarray, model, min_conf: float) -> List[Dict]:
145
+ logger.info("Processing image...")
146
+
147
+ # PaddleOCR Predict
148
  result = model.ocr(img)
149
 
 
 
150
  if not result or result[0] is None:
151
+ logger.warning("No text found.")
152
  return []
153
 
154
  raw_results = result[0]
155
 
156
+ # تصفية النتائج
157
  filtered_results = []
158
  for item in raw_results:
 
159
  if not isinstance(item, (list, tuple)) or len(item) < 2:
160
  continue
 
161
  text_part = item[1]
 
 
162
  if isinstance(text_part, (list, tuple)) and len(text_part) >= 2:
163
  text, conf = text_part
164
+ # نقبل النص إذا تجاوز نسبة الثقة المطلوبة
165
  if conf >= min_conf:
166
  filtered_results.append(item)
167
+
168
+ # تطبيق الترتيب العربي (RTL)
169
+ sorted_boxes = organize_boxes_rtl(filtered_results)
170
 
171
  final_output = []
172
  for i, box in enumerate(sorted_boxes):
173
+ # تنظيف النص باستخدام الدالة المخصصة للعقود
174
+ clean_txt = clean_contract_text(box["text"])
175
+
176
+ # إذا كان النص فارغاً بعد التنظيف، نستخدم النص الأصلي كاحتياط
177
+ # هذا يمنع فقدان البيانات إذا أخطأ التنظيف
178
+ final_text = clean_txt if clean_txt else box["text"]
179
+
180
+ if final_text.strip():
181
  final_output.append({
182
  "box_id": i + 1,
183
+ "text": final_text,
184
+ # "original_text": box["text"], # يمكن تفعيل هذا للدييباج
185
  "confidence": round(box["confidence"], 4),
186
  "bbox": box["bbox"]
187
  })
 
190
 
191
  @app.get("/")
192
  def root():
193
+ return {"name": "Arabic Contract OCR API", "status": "Ready", "pdf_support": PDF_AVAILABLE}
 
 
 
 
194
 
195
  @app.post("/ocr")
196
  async def ocr_image(
197
  file: UploadFile = File(...),
198
+ min_conf: float = Query(default=0.4, ge=0.0, le=1.0), # مناسب للمستندات الواضحة نوعاً ما
199
  ):
200
  try:
201
  contents = await file.read()
 
207
  model = get_model()
208
  ocr_results = process_image_full(img, model, min_conf)
209
 
210
+ # دمج النصوص: كل عنصر في سطر جديد للحفاظ على شكل العقد
211
  full_text = "\n".join([r["text"] for r in ocr_results])
212
 
213
  return {
214
  "items": ocr_results,
215
  "text": full_text,
216
+ "total_lines": len(ocr_results)
217
  }
218
 
219
  @app.post("/ocr-pdf")
220
  async def ocr_pdf(
221
  file: UploadFile = File(...),
222
  dpi: int = Query(default=300, ge=72, le=600),
223
+ min_conf: float = Query(default=0.4, ge=0.0, le=1.0),
224
  ):
225
  if not PDF_AVAILABLE:
226
+ raise HTTPException(status_code=500, detail="PDF support not available (install pdf2image & poppler)")
227
 
228
  try:
229
  contents = await file.read()
230
  pages = convert_from_bytes(contents, dpi=dpi)
231
  except Exception as e:
232
+ raise HTTPException(status_code=400, detail=f"Invalid PDF: {e}")
233
 
234
  model = get_model()
235
  all_results = []
 
237
 
238
  for page_num, pil_img in enumerate(pages, start=1):
239
  img = cv2.cvtColor(np.array(pil_img.convert("RGB")), cv2.COLOR_RGB2BGR)
 
240
  page_results = process_image_full(img, model, min_conf)
241
 
242
  for item in page_results:
243
  item["page"] = page_num
244
 
245
  all_results.extend(page_results)
246
+
247
+ page_txt = "\n".join([r["text"] for r in page_results])
248
+ if page_txt:
249
+ all_text.append(f"--- Page {page_num} ---\n{page_txt}")
250
 
251
  return {
252
  "pages": len(pages),
253
  "items": all_results,
254
  "text": "\n\n".join(all_text),
255
+ "total_lines": len(all_results)
256
  }
257
 
258
  if __name__ == "__main__":