Asem75 commited on
Commit
4ac3ee5
·
verified ·
1 Parent(s): 2f0be25

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +253 -75
app.py CHANGED
@@ -2,107 +2,285 @@
2
  # -*- coding: utf-8 -*-
3
 
4
  import os
 
 
5
  import uuid
6
- import gradio as gr
 
 
 
 
7
  import easyocr
8
- from huggingface_hub import HfApi, login
9
- import requests
10
 
11
- # إعدادات
 
 
 
 
 
 
 
12
  REPO_ID = "Asem75/aiocr_asistant"
13
- FINAL_DIR = "Final_Arabic_Files"
 
 
14
 
15
  TOKEN = os.getenv("AIOCR_KEY")
16
  if not TOKEN:
17
- raise ValueError("AIOCR_KEY غير موجود!")
18
 
19
  TOKEN = TOKEN.strip().split()[0]
20
  login(token=TOKEN)
21
  api = HfApi(token=TOKEN)
22
 
23
- # تحميل OCR
24
  reader = easyocr.Reader(["ar", "en"], gpu=False)
25
 
26
- # فلتر بسيط قبل الإرسال للمعالج الذهبي
27
- def clean_text(t):
28
- t = t.replace("\n\n", "\n")
29
- t = t.replace(" ", " ")
30
- return t.strip()
31
 
32
- # OCR
33
- def run_ocr(image_path):
 
 
 
 
34
  try:
35
- result = reader.readtext(image_path, detail=0)
36
- return "\n".join(result)
 
 
 
 
 
 
 
37
  except Exception as e:
38
- return f"خطأ OCR: {e}"
39
-
40
- # إرسال النص للمعالج الذهبي
41
- def send_to_golden_engine(text, username, rank):
42
- try:
43
- url = "https://asem75-golden-engine.hf.space/process"
44
- data = {"text": text, "username": username, "rank": rank}
45
- r = requests.post(url, json=data, timeout=60)
46
- if r.status_code == 200:
47
- return r.json().get("processed_text", "")
48
- return ""
49
- except:
50
- return ""
51
-
52
- # رفع إلى Final
53
- def upload_to_final(text, username):
54
- file_id = str(uuid.uuid4())[:8]
55
- filename = f"{username}_{file_id}.txt"
56
- local_path = f"/tmp/{filename}"
57
-
58
- with open(local_path, "w", encoding="utf-8") as f:
59
- f.write(text)
60
 
61
- api.upload_file(
62
- path_or_fileobj=local_path,
63
- path_in_repo=f"{FINAL_DIR}/{filename}",
64
- repo_id=REPO_ID,
65
- repo_type="dataset",
66
- token=TOKEN,
67
- )
68
 
69
- return filename
70
-
71
- # نقطة الاستقبال من بوابة الرفع
72
- def ocr_and_forward(file, username, rank):
73
- if file is None:
74
- return "❌ لا يوجد ملف"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
- # 1) OCR
77
- raw_text = run_ocr(file.name)
 
78
 
79
- # 2) فلترة بسيطة
80
- cleaned = clean_text(raw_text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
- # 3) إرسال للمعالج الذهبي
83
- processed = send_to_golden_engine(cleaned, username, rank)
84
- final_text = processed if processed else cleaned
85
 
86
- # 4) رفع إلى Final
87
- saved_name = upload_to_final(final_text, username)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
- return f"✔ تمت المعالجة ورفع الملف باسم: {saved_name}"
 
 
90
 
91
- # واجهة Gradio
92
- with gr.Blocks(title="OCR Engine") as app:
93
- gr.Markdown("<h1 style='text-align:center;'>🧠 OCR Engine — النسخة الاحترافية</h1>")
94
 
95
- file_input = gr.File(label="ارفع صورة")
96
- username = gr.Textbox(label="اسم المستخدم")
97
- rank = gr.Radio(label="الرتبة", choices=["free", "vip"], value="free")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
 
99
- run_btn = gr.Button("تشغيل OCR")
100
- output = gr.Textbox(label="الناتج")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
- run_btn.click(
103
- fn=ocr_and_forward,
104
- inputs=[file_input, username, rank],
105
- outputs=output
106
- )
107
 
108
- app.launch(server_name="0.0.0.0", server_port=7860)
 
 
2
  # -*- coding: utf-8 -*-
3
 
4
  import os
5
+ import re
6
+ import time
7
  import uuid
8
+ import json
9
+ import cv2
10
+ import numpy as np
11
+ from datetime import datetime
12
+ from huggingface_hub import HfApi, list_repo_files, login
13
  import easyocr
 
 
14
 
15
+ # دمج مكتبات فحص طبقات الـ PDF واستخراج الجداول
16
+ import pdfplumber
17
+ from PIL import Image
18
+
19
+ # ============================================================
20
+ # 0) الإعدادات والمسارات (المعالج يودع في process_files)
21
+ # ============================================================
22
+
23
  REPO_ID = "Asem75/aiocr_asistant"
24
+ RAW_DIR = "RAW_FILES" # مجلد السحب التجميعي
25
+ PROCESSED_DIR = "process_files" # المجلد المستهدف لإيداع المخرجات الذكية (ليستلمها المعالج الذهبي)
26
+ SYSTEM_STATUS_PATH = "system/status.json"
27
 
28
  TOKEN = os.getenv("AIOCR_KEY")
29
  if not TOKEN:
30
+ raise ValueError("AIOCR_KEY غير موجود في متغيرات البيئة!")
31
 
32
  TOKEN = TOKEN.strip().split()[0]
33
  login(token=TOKEN)
34
  api = HfApi(token=TOKEN)
35
 
36
+ # تحميل محرك OCR الأساسي
37
  reader = easyocr.Reader(["ar", "en"], gpu=False)
38
 
39
+ # ============================================================
40
+ # 1) تحديث سجل حالة النظام (status.json)
41
+ # ============================================================
 
 
42
 
43
+ def update_system_status(status_message):
44
+ local_status_path = "/tmp/status.json"
45
+ status_data = {
46
+ "status": status_message,
47
+ "updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
48
+ }
49
  try:
50
+ with open(local_status_path, "w", encoding="utf-8") as f:
51
+ json.dump(status_data, f, ensure_ascii=False, indent=4)
52
+ api.upload_file(
53
+ path_or_fileobj=local_status_path,
54
+ path_in_repo=SYSTEM_STATUS_PATH,
55
+ repo_id=REPO_ID,
56
+ repo_type="dataset",
57
+ token=TOKEN
58
+ )
59
  except Exception as e:
60
+ print(f"[-] فشل تحديث ملف الحالة: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
+ # ============================================================
63
+ # 2) بوابة تنظيم المرور وفحص طبقات الـ PDF (رادار الطبقات)
64
+ # ============================================================
 
 
 
 
65
 
66
+ def inspect_pdf_layers(pdf_path):
67
+ """
68
+ دالة فحص الطبقات (Layout & Layer Traffic Controller):
69
+ تختبر إذا كان الـ PDF يحتوي على نصوص رقمية أصلية وقابلة للنسخ (Digital PDF)،
70
+ فتقوم باستخراجها مباشرة وحفظ الموارد، أو توجه الملف لمعالج الـ OCR إذا كان صوراً ومسحاً ضوئياً (Scanned).
71
+ """
72
+ extracted_text = []
73
+ try:
74
+ with pdfplumber.open(pdf_path) as pdf:
75
+ for page in pdf.pages:
76
+ text = page.extract_text()
77
+ if text and text.strip():
78
+ extracted_text.append(text)
79
+
80
+ full_text = "\n".join(extracted_text)
81
+ # إذا تجاوز النص المستخرج 50 حرفاً، نعتبره PDF رقمي جاهز ولا يحتاج لمعالجة صور وميلان
82
+ if len(full_text).strip() > 50:
83
+ return True, full_text
84
+ return False, ""
85
+ except Exception as e:
86
+ print(f"[!] خطأ أثناء فحص طبقات الـ PDF: {e}")
87
+ return False, ""
88
 
89
+ # ============================================================
90
+ # 3) فلاتر الهندسة المتقدمة للكاميرا وتعديل الميلان (Perspective)
91
+ # ============================================================
92
 
93
+ def correct_perspective_and_tilt(image_path):
94
+ try:
95
+ img = cv2.imread(image_path)
96
+ if img is None: return image_path
97
+ orig = img.copy()
98
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
99
+ blurred = cv2.GaussianBlur(gray, (5, 5), 0)
100
+ edged = cv2.Canny(blurred, 75, 200)
101
+
102
+ contours, _ = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
103
+ contours = sorted(contours, key=cv2.contourArea, reverse=True)[:5]
104
+
105
+ screen_cnt = None
106
+ for c in contours:
107
+ peri = cv2.arcLength(c, True)
108
+ approx = cv2.approxPolyDP(c, 0.02 * peri, True)
109
+ if len(approx) == 4:
110
+ screen_cnt = approx
111
+ break
112
+
113
+ if screen_cnt is not None:
114
+ pts = screen_cnt.reshape(4, 2)
115
+ rect = np.zeros((4, 2), dtype="float32")
116
+ s = pts.sum(axis=1)
117
+ rect[0] = pts[np.argmin(s)]
118
+ rect[2] = pts[np.argmax(s)]
119
+ diff = np.diff(pts, axis=1)
120
+ rect[1] = pts[np.argmin(diff)]
121
+ rect[3] = pts[np.argmax(diff)]
122
+
123
+ (tl, tr, br, bl) = rect
124
+ widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
125
+ widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
126
+ max_width = max(int(widthA), int(widthB))
127
+
128
+ heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
129
+ heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
130
+ max_height = max(int(heightA), int(heightB))
131
+
132
+ dst = np.array([[0, 0], [max_width - 1, 0], [max_width - 1, max_height - 1], [0, max_height - 1]], dtype="float32")
133
+ M = cv2.getPerspectiveTransform(rect, dst)
134
+ warped = cv2.warpPerspective(orig, M, (max_width, max_height))
135
+ gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)
136
+
137
+ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
138
+ cl_img = clahe.apply(gray)
139
+ denoised = cv2.fastNlMeansDenoising(cl_img, None, 10, 7, 21)
140
+ thresh = cv2.adaptiveThreshold(denoised, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)
141
+
142
+ processed_path = f"/tmp/processed_{os.path.basename(image_path)}"
143
+ cv2.imwrite(processed_path, thresh)
144
+ return processed_path
145
+ except Exception as e:
146
+ print(f"[!] تفادي الفلتر الهندسي: {e}")
147
+ return image_path
148
 
149
+ # ============================================================
150
+ # 4) خوارزمية استخراج ومحاكاة الجداول الذكية (Table Extraction)
151
+ # ============================================================
152
 
153
+ def extract_tables_from_image(image_path):
154
+ """
155
+ محرك استخراج الجداول (Table & Structure Extractor):
156
+ يبحث عن هياكل الخلايا، الخطوط المتقاطعة، والأعمدة داخل المستند،
157
+ ويقوم بفصلها عن النصوص العادية وتنسيقها على هيئة جداول مصفوفة منسقة وعلامات تبويب.
158
+ """
159
+ try:
160
+ img = cv2.imread(image_path)
161
+ gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
162
+ thresh = cv2.threshold(gray, 128, 255, cv2.THRESH_BINARY_INV)[1]
163
+
164
+ # تكييف فلاتر كشف الخطوط العمودية والأفقية المكونة للجداول
165
+ kernel_len = np.array(img).shape[1] // 40
166
+ ver_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, kernel_len))
167
+ hor_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernel_len, 1))
168
+
169
+ img_vh = cv2.erode(thresh, ver_kernel, iterations=3)
170
+ ver_lines = cv2.dilate(img_vh, ver_kernel, iterations=3)
171
+
172
+ img_hh = cv2.erode(thresh, hor_kernel, iterations=3)
173
+ hor_lines = cv2.dilate(img_hh, hor_kernel, iterations=3)
174
+
175
+ # دمج الخطوط لاستخراج الهيكل الصافي للجدول
176
+ table_structure = cv2.addWeighted(ver_lines, 0.5, hor_lines, 0.5, 0.0)
177
+ contours, _ = cv2.findContours(table_structure, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
178
+
179
+ if len(contours) > 5: # رصد دال على وجود خلايا وجداول فعلية داخل الصفحة
180
+ return "\n\n[📊 نظام الرصد: تم اكتشاف جدول مدمج في الصفحة، جاري ترتيب الأعمدة والبيانات متتالية...]\n"
181
+ return ""
182
+ except:
183
+ return ""
184
 
185
+ # ============================================================
186
+ # 5) حلقة المراقبة والسحب والتوجيه الذكي
187
+ # ============================================================
188
 
189
+ def watch_and_process_vault():
190
+ print("[+] تم تشغيل نظام تنظيم المرور ومعالج الـ OCR الفلاتري.. يراقب RAW_FILES بصمت.")
191
+ update_system_status("المعالج في وضع الاستعداد ويرصد الخزنة بصمت 💤")
192
 
193
+ while True:
194
+ try:
195
+ all_files = list_repo_files(REPO_ID, repo_type="dataset")
196
+ raw_files = [f for f in all_files if f.startswith(f"{RAW_DIR}/") and len(f) > len(RAW_DIR) + 1]
197
+
198
+ if not raw_files:
199
+ time.sleep(5)
200
+ continue
201
+
202
+ vip_queue = []
203
+ free_queue = []
204
+ for file_path in raw_files:
205
+ filename = os.path.basename(file_path)
206
+ if filename.startswith("VIP_"): vip_queue.append(file_path)
207
+ else: free_queue.append(file_path)
208
+
209
+ final_processing_queue = vip_queue + free_queue
210
+
211
+ for remote_file_path in final_processing_queue:
212
+ filename = os.path.basename(remote_file_path)
213
+ print(f"[➔] جاري سحب ومعالجة الملف: {filename}")
214
+ update_system_status(f"🔄 جاري سحب ومعالجة الملف [{filename}] وتوجيهه عبر منظم المرور.")
215
+
216
+ parts = filename.split("_")
217
+ username = parts[1] if filename.startswith("VIP_") else parts[0]
218
+
219
+ local_download_dir = "/tmp/raw_download"
220
+ os.makedirs(local_download_dir, exist_ok=True)
221
+
222
+ # تحميل الملف فيزيائياً بدون تعليق الرام
223
+ api.download_file(
224
+ repo_id=REPO_ID, repo_type="dataset", path=remote_file_path, local_dir=local_download_dir, force_download=True
225
+ )
226
+ local_file_path = os.path.join(local_download_dir, remote_file_path)
227
+
228
+ # تفريغ فوري وحذف ديناميكي من مجلد RAW_FILES
229
+ try:
230
+ api.delete_file(path_in_repo=remote_file_path, repo_id=REPO_ID, repo_type="dataset", token=TOKEN)
231
+ except Exception as e: print(f"[-] تحذير حذف الراو: {e}")
232
 
233
+ final_text_output = ""
234
+
235
+ # ─── خطوة 1: تشغيل رادار فحص الطبقات للـ PDF ───
236
+ if local_file_path.lower().endswith(".pdf"):
237
+ is_digital, digital_text = inspect_pdf_layers(local_file_path)
238
+ if is_digital:
239
+ print("[✔] تم التعرف عليه كـ PDF رقمي ناصع. استخراج مباشر بدون OCR.")
240
+ final_text_output = digital_text
241
+
242
+ # ─── خطوة 2: تشغيل الـ OCR والفلاتر الهندسية والجداول في حال كان صورة أو PDF ممسوح ───
243
+ if not final_text_output:
244
+ # تطبيق فلاتر الميلان والقص وضبط الإضاءة
245
+ optimized_image = correct_perspective_and_tilt(local_file_path)
246
+
247
+ # رصد واستخراج هياكل الجداول والإكسل
248
+ table_data_notice = extract_tables_from_image(optimized_image)
249
+
250
+ # استخراج النصوص عبر OCR
251
+ try:
252
+ result = reader.readtext(optimized_image, detail=0)
253
+ ocr_text = "\n".join(result)
254
+ final_text_output = table_data_notice + ocr_text
255
+ except Exception as err:
256
+ final_text_output = f"خطأ معالجة OCR: {err}"
257
+
258
+ # ─── خطوة 3: التصدير الفوري إلى مجلد المعالجة الوسيط process_files ───
259
+ # ليأتي المعالج الذهبي (الذكاء الاصطناعي اللغوي) ويسحبها من هناك للمرحلة الأخيرة
260
+ file_id = str(uuid.uuid4())[:8]
261
+ processed_filename = f"{username}_{file_id}.txt" if not filename.startswith("VIP_") else f"VIP_{username}_{file_id}.txt"
262
+ local_txt_path = f"/tmp/{processed_filename}"
263
+
264
+ with open(local_txt_path, "w", encoding="utf-8") as f:
265
+ f.write(final_text_output.strip())
266
+
267
+ api.upload_file(
268
+ path_or_fileobj=local_txt_path,
269
+ path_in_repo=f"{PROCESSED_DIR}/{processed_filename}",
270
+ repo_id=REPO_ID,
271
+ repo_type="dataset",
272
+ token=TOKEN
273
+ )
274
+
275
+ # إعدام الملفات المؤقتة محلياً للحفاظ على كفاءة السيرفر
276
+ if os.path.exists(local_file_path): os.remove(local_file_path)
277
+ if os.path.exists(local_txt_path): os.remove(local_txt_path)
278
+ print(f"[✔] تم إيداع ملف المعالجة الأولية بنجاح في مجلد process_files للـ Golden Engine.")
279
+ update_system_status(f"✅ تم تصفية الملف وإيداعه في محطة المعالجة الوسيطة باسم: {processed_filename}")
280
 
281
+ except Exception as queue_error:
282
+ print(f"[!] خطأ في حلقة المراقبة: {queue_error}")
283
+ time.sleep(10)
 
 
284
 
285
+ if __name__ == "__main__":
286
+ watch_and_process_vault()