Asem75 commited on
Commit
2bfcbfa
·
verified ·
1 Parent(s): 16d7dac

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +221 -374
app.py CHANGED
@@ -10,11 +10,8 @@ from datetime import datetime, timedelta
10
  from urllib.parse import unquote
11
 
12
  import gradio as gr
13
- import requests
14
  from huggingface_hub import HfApi, list_repo_files, login, hf_hub_download
15
 
16
- from converter import convert_text, zip_single_file
17
-
18
  # ============================================================
19
  # 0) إعدادات البيئة والنظام العامة
20
  # ============================================================
@@ -25,15 +22,16 @@ ERROR_LOG_PATH = "system/error_log.txt"
25
  os.makedirs("system", exist_ok=True)
26
 
27
  REPO_ID = "Asem75/aiocr_asistant"
28
-
29
  RAW_DIR = "RAW_FILES"
30
  WATCH_DIR_1 = "process"
31
  WATCH_DIR_2 = "process_files"
32
-
33
  FINAL_DIR = "Final_Arabic_Files"
34
  SYSTEM_STATUS_PATH = "system/status.json"
35
- ABOUT_APP_PATH = "system/about_app.json" # ✅ جديد: تفاصيل الاشتراك / محتوى يكتبه المدير
36
- MESSAGES_PATH = "system/messages.json" # ✅ جديد: قناة تقارير الواجهة لمكتب المدير
 
 
 
37
 
38
  TOKEN = os.getenv("AIOCR_KEY")
39
  if not TOKEN:
@@ -59,186 +57,170 @@ def sanitize_filename(name: str) -> str:
59
  name = name.replace(" ", "_")
60
  return re.sub(r"[^\w\.\-]", "", name)
61
 
62
- def generate_guest_name() -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  """
64
- 🧩 توليد معرّف مؤقت لكل جلسة بانتظار دمج "كود التعرف" مستقبلاً
65
- (الاسم + البريد الإلكتروني + بصمة عتاد الجهاز + الرتبة).
66
- عند إضافة ذلك الكود، يكفي تمرير بياناته الحقيقية إلى user_session
67
- بدلاً من هذا المعرّف المؤقت، وسيُحدَّث الترحيب وأيقونة الرتبة تلقائياً.
 
 
 
 
 
 
 
 
 
 
68
  """
69
- return "زائر_" + secrets.token_hex(3)
70
 
71
- # ------------------------------------------------------------
72
- # ✅ (1) تحديث ملف status.json في الخزنة السحابية بعد كل عملية رفع
73
- # ------------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  def update_status_json(status_text: str):
75
  try:
76
  local_path = os.path.join("system", "status.json")
77
- data = {
78
- "status": status_text,
79
- "updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
80
- }
81
  with open(local_path, "w", encoding="utf-8") as f:
82
  json.dump(data, f, ensure_ascii=False, indent=2)
83
-
84
- api.upload_file(
85
- path_or_fileobj=local_path,
86
- path_in_repo=SYSTEM_STATUS_PATH,
87
- repo_id=REPO_ID,
88
- repo_type="dataset",
89
- token=TOKEN,
90
- )
91
- except Exception as e:
92
  log_error(f"STATUS UPDATE ERROR: {e}")
93
 
94
- # ------------------------------------------------------------
95
- # ✅ (4) إرسال تقرير من الواجهة إلى مكتب المدير عبر system/messages.json
96
- # ------------------------------------------------------------
97
  def send_report_to_manager(report_text: str):
98
  try:
99
  try:
100
- local_existing = hf_hub_download(
101
- repo_id=REPO_ID,
102
- filename=MESSAGES_PATH,
103
- repo_type="dataset",
104
- token=TOKEN,
105
- )
106
- with open(local_existing, "r", encoding="utf-8") as f:
107
  messages = json.load(f)
108
- if not isinstance(messages, list):
109
- messages = []
110
- except Exception:
111
  messages = []
112
-
113
- messages.append({
114
- "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
115
- "from": "upload_interface",
116
- "report": report_text,
117
- })
118
-
119
  local_path = os.path.join("system", "messages.json")
120
- with open(local_path, "w", encoding="utf-8") as f:
121
  json.dump(messages, f, ensure_ascii=False, indent=2)
122
-
123
- api.upload_file(
124
- path_or_fileobj=local_path,
125
- path_in_repo=MESSAGES_PATH,
126
- repo_id=REPO_ID,
127
- repo_type="dataset",
128
- token=TOKEN,
129
- )
130
- except Exception as e:
131
  log_error(f"REPORT SEND ERROR: {e}")
132
 
133
- # ------------------------------------------------------------
134
- # ✅ (3) تحميل تفاصيل الاشتراك من system/about_app.json (يكتبها المدير)
135
- # ------------------------------------------------------------
136
- def load_about_app(rank: str = "free") -> str:
137
- data = {}
138
  try:
139
  local_path = hf_hub_download(
140
  repo_id=REPO_ID,
141
- filename=ABOUT_APP_PATH,
142
  repo_type="dataset",
143
  token=TOKEN,
 
144
  )
145
  with open(local_path, "r", encoding="utf-8") as f:
146
  data = json.load(f)
147
- except Exception as e:
148
- log_error(f"ABOUT APP READ ERROR: {e}")
149
-
150
- if isinstance(data, dict):
151
- section_text = (
152
- data.get(rank)
153
- or data.get("message")
154
- or data.get("about")
155
- or "لا تتوفر تفاصيل إضافية حالياً."
156
- )
157
- else:
158
- section_text = "لا تتوفر تفاصيل إضافية حالياً."
159
-
160
- badge = "🥇 عضوية VIP" if rank == "vip" else "🥈 عضوية مجانية (Free)"
161
-
162
- html = f"""
163
- <div role="dialog" aria-label="تفاصيل الاشتراك"
164
- style="background:#ffffff; border:1px solid #ddd; border-radius:8px; padding:16px; margin-top:8px;">
165
- <h3 style="margin-top:0;">{badge}</h3>
166
- <p style="white-space:pre-line; margin:0;">{section_text}</p>
167
- </div>
168
- """
169
- return html
170
-
171
- # ============================================================
172
- # 2) رادار فحص القيود للمشترك المجاني (FREE)
173
- # ============================================================
174
-
175
- def check_free_user_limits(username):
176
- try:
177
- all_files = list_repo_files(REPO_ID, repo_type="dataset")
178
-
179
- raw_prefix = f"{RAW_DIR}/{username}_"
180
- watch1_prefix = f"{WATCH_DIR_1}/{username}_"
181
- watch2_prefix = f"{WATCH_DIR_2}/{username}_"
182
-
183
- active_files = [
184
- f for f in all_files
185
- if f.startswith(raw_prefix) or f.startswith(watch1_prefix) or f.startswith(watch2_prefix)
186
- ]
187
-
188
- if active_files:
189
- return False, f"❌ الرادار رصد وجود {len(active_files)} ملفات لك قيد المعالجة حالياً. يرجى الانتظار."
190
-
191
- upload_count = 0
192
- total_size_bytes = 0
193
- time_limit = datetime.now() - timedelta(days=1)
194
-
195
- if os.path.exists(ERROR_LOG_PATH):
196
- with open(ERROR_LOG_PATH, "r", encoding="utf-8") as f:
197
- for line in f:
198
- if f"SUCCESS_UPLOAD_FREE:{username}" in line:
199
- try:
200
- time_str = line.split("]")[0].replace("[", "").strip()
201
- log_time = datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S")
202
-
203
- if log_time > time_limit:
204
- upload_count += 1
205
- size_part = line.split("SIZE:")[-1].strip()
206
- total_size_bytes += int(size_part)
207
- except:
208
- continue
209
-
210
- if upload_count >= 5:
211
- return False, "❌ عذراً، لقد استهلكت حدك الأقصى وهو 5 ملفات خلال الـ 24 ساعة الماضية."
212
-
213
- if total_size_bytes >= 3 * 1024 * 1024:
214
- return False, "❌ عذراً، لقد استهلكت حد الحجم الأقصى وهو 3 ميجابايت خلال الـ 24 ساعة الماضية."
215
-
216
- return True, "مسموح بالرفع"
217
- except Exception as e:
218
- log_error(f"LIMIT CHECK ERROR for {username}: {e}")
219
- return False, "⚠️ حدث خطأ غير متوقع أثناء فحص رادار القيود."
220
-
221
- # ============================================================
222
- # 3) قراءة وعرض حالة النظام
223
- # ============================================================
224
-
225
- def read_status_json():
226
- try:
227
- api.download_file(
228
- repo_id=REPO_ID,
229
- repo_type="dataset",
230
- path=SYSTEM_STATUS_PATH,
231
- local_dir="./",
232
- force_download=True,
233
- )
234
- with open("./system/status.json", "r", encoding="utf-8") as f:
235
- data = json.load(f)
236
 
237
  status_text = data.get('status', 'غير معروف')
238
  updated_text = data.get('updated', 'غير معروف')
239
 
240
- # ✅ (2) مربع الحالة الآن متوافق مع قارئ الشاشة:
241
- # role="status" + aria-live="polite" تجعل البرنامج يقرأ كل تحديث تلقائياً
242
  html = f"""
243
  <div role="status" aria-live="polite" aria-atomic="true"
244
  style="font-size:16px; background-color: #f0f7ff; padding: 10px; border-radius: 5px; border: 1px solid #b3d7ff;">
@@ -249,30 +231,30 @@ def read_status_json():
249
  return html
250
  except Exception as e:
251
  log_error(f"STATUS READ ERROR: {e}")
252
- return f"""<div role="status" aria-live="polite" style='color:orange;'>⚠️ لا يمكن قراءة ملف status.json حالياً — {e}</div>"""
253
 
254
- # ============================================================
255
- # 4) معالجة رفع الملفات مع دعم جميع البادئات
256
- # ============================================================
 
 
 
257
 
258
- def handle_upload(files, session,
259
- enable_table, enable_handwriting,
260
- enable_heavy_prep, enable_high_dpi,
261
- enable_fast, enable_tashkeel):
262
- if not files:
263
  return "<div role='alert'>❌ يرجى اختيار ملفات للرفع</div>"
264
 
265
- session = session or {}
266
- # ⚠️ مؤقتاً: الاسم يولَّد تلقائياً للجلسة. سيُستبدل باسم المستخدم الحقيقي
267
- # فور دمج "كود التعرف" (الاسم + البريد + بصمة الجهاز + الرتبة).
268
- username = sanitize_filename((session.get("name") or generate_guest_name()).strip())
269
  rank = session.get("rank", "free")
270
  is_vip = (rank == "vip")
271
 
272
  if not is_vip:
273
  allowed, message = check_free_user_limits(username)
274
- if not allowed:
275
- return f"<div role='alert' style='color:red; font-weight:bold;'>{message}</div>"
276
 
277
  success = 0
278
  prefix = f"VIP_{username}_" if is_vip else f"{username}_"
@@ -280,259 +262,124 @@ def handle_upload(files, session,
280
  for file in files:
281
  try:
282
  fname = sanitize_filename(unquote(os.path.basename(file.name)))
283
- file_size = os.path.getsize(file.name)
284
-
285
- if not is_vip and file_size > 3 * 1024 * 1024:
286
- return "<div role='alert' style='color:red;'>❌ خطأ: أحد الملفات يتجاوز حجمه 3 ميجابايت!</div>"
287
-
288
- # بناء بادئة الخيارات
289
- option_prefix = ""
290
- if enable_fast:
291
- option_prefix = "fast_"
292
- else:
293
- if enable_table:
294
- option_prefix += "tbl_"
295
- if enable_handwriting:
296
- option_prefix += "hw_"
297
- if enable_heavy_prep:
298
- option_prefix += "prep_"
299
- if enable_high_dpi:
300
- option_prefix += "dpi300_"
301
- if enable_tashkeel:
302
- option_prefix += "tash_"
303
-
304
  final_filename = f"{prefix}{option_prefix}{fname}"
305
-
306
- # الرفع إلى raw_files
307
- api.upload_file(
308
- path_or_fileobj=file.name,
309
- path_in_repo=f"{RAW_DIR}/{final_filename}",
310
- repo_id=REPO_ID,
311
- repo_type="dataset",
312
- token=TOKEN,
313
- )
314
 
315
- if not is_vip:
316
- log_error(f"SUCCESS_UPLOAD_FREE:{username} | FILE:{final_filename} | SIZE:{file_size}")
317
- else:
318
- log_error(f"SUCCESS_UPLOAD_VIP:{username} | FILE:{final_filename}")
319
-
320
  success += 1
 
 
321
 
322
- except Exception as e:
323
- log_error(f"UPLOAD ERROR: {file.name} — {e}")
324
-
325
- # ✅ (1) كتابة "تم رفع الملف" في system/status.json على الخزنة السحابية
326
  if success > 0:
327
- update_status_json("تم رفع الملف" if success == 1 else f"تم رفع {success} ملفات")
328
-
329
- # (4) إرسال تقرير بهذه العملية إلى مكتب المدير (system/messages.json)
330
- send_report_to_manager(
331
- f"المستخدم {username} ({'VIP' if is_vip else 'FREE'}) رفع {success} من {len(files)} ملف/ات."
332
- )
333
-
334
- return f"<div role='status' style='color:green; font-weight:bold;'>تم رفع {success} ملف/ات مع الخيارات المحددة ✔</div>"
335
 
336
  # ============================================================
337
- # 5) إدارة وقراءة ملفات Final والتحويل (باستخدام hf_hub_download)
338
  # ============================================================
339
 
340
- def list_final_files():
341
- """جلب قائمة الملفات من Final_Arabic_Files في المستودع"""
342
- try:
343
- files = list_repo_files(REPO_ID, repo_type="dataset")
344
- final_files = [f.split("/", 1)[1] for f in files if f.startswith(FINAL_DIR + "/")]
345
- return sorted(final_files) if final_files else []
346
- except Exception as e:
347
- log_error(f"FINAL LIST ERROR: {e}")
348
- return []
349
-
350
- def load_file_content(selected_name: str):
351
- """تحميل محتوى الملف المختار من Final_Arabic_Files وعرضه في مربع التعديل"""
352
- if not selected_name:
353
- return "", "", ""
354
- try:
355
- remote_path = f"{FINAL_DIR}/{selected_name}"
356
- # ✅ استخدام hf_hub_download المتوافق مع جميع الإصدارات
357
- local_path = hf_hub_download(
358
- repo_id=REPO_ID,
359
- filename=remote_path,
360
- repo_type="dataset",
361
- token=TOKEN
362
- )
363
- with open(local_path, "r", encoding="utf-8") as f:
364
- text = f.read()
365
- return text, os.path.splitext(selected_name)[0], selected_name
366
- except Exception as e:
367
- log_error(f"FINAL LOAD ERROR: {selected_name} — {e}")
368
- return "❌ فشل تحميل الملف", "", ""
369
-
370
- def convert_and_upload(text, base_name, target_ext, font_name, font_size):
371
- if not text.strip(): return "❌ لا يوجد نص", None
372
- try:
373
- out_path = convert_text(text, base_name, target_ext, font_name, font_size)
374
- new_name = os.path.basename(out_path)
375
- api.upload_file(
376
- path_or_fileobj=out_path,
377
- path_in_repo=f"{FINAL_DIR}/{new_name}",
378
- repo_id=REPO_ID, repo_type="dataset", token=TOKEN
379
- )
380
- return f"✅ تم التحويل والرفع باسم {new_name}", out_path
381
- except Exception as e:
382
- log_error(f"CONVERT/UPLOAD ERROR: {e}")
383
- return f"⚠️ فشل: {e}", None
384
-
385
- def do_zip(text, base_name):
386
- if not text.strip(): return "❌ لا يوجد نص", None
387
- try:
388
- txt_path = convert_text(text, base_name, "txt")
389
- return "✅ تم إنشاء ZIP", zip_single_file(txt_path, base_name)
390
- except Exception as e:
391
- return "⚠️ فشل إنشاء ZIP", None
392
-
393
- # ============================================================
394
- # 5.b) الجلسة الحالية: ترحيب بالاسم + أيقونة الرتبة
395
- # (نقطة الدخول المستقبلية لـ "كود التعرف")
396
- # ============================================================
397
-
398
- def init_session():
399
  """
400
- 🧩 يُستدعى عند تحميل الصفحة. حالياً يولّد جلسة "زائر" مؤقتة بالرتبة free.
401
- عندما يُضاف "كود التعرف" (اسم + بريد + بصمة جهاز + رتبة)، يكفي استبدال
402
- منطق هذه الدالة بنتيجة ذلك الكود، وسيُحدَّث الترحيب + الأيقونة + الشاشة
403
- المعروضة تلقائياً بحسب الرتبة (vip / free) دون أي تغيير آخر في الواجهة.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  """
405
- session = {"name": generate_guest_name(), "email": "", "device_id": "", "rank": "free"}
406
- welcome_html = f"<p role='status' style='font-size:16px; margin:0;'>👋 مرحباً بك، <b>{session['name']}</b></p>"
407
- icon_label = "🥇 VIP" if session["rank"] == "vip" else "🥈 FREE"
408
- return session, welcome_html, icon_label
 
 
 
 
 
409
 
410
  def toggle_subscription_panel(session, is_visible):
411
- """فتح/إغلاق شاشة تفاصيل الاشتراك المرتبطة بـ system/about_app.json"""
 
412
  new_visible = not is_visible
413
  if new_visible:
414
- rank = (session or {}).get("rank", "free")
415
- return gr.update(value=load_about_app(rank), visible=True), new_visible
416
  return gr.update(visible=False), new_visible
417
 
418
  # ============================================================
419
- # 6) واجهات Gradio مع جميع الخيارات
420
  # ============================================================
421
 
422
  with gr.Blocks(title="📤 بوابة الرفع") as upload_page:
423
- gr.Markdown("<h1 style='text-align:center;'>📤 بوابة الرفع ودورة حياة الملفات</h1>")
424
 
425
- # (5) أُزيل مربع "اسم المستخدم" التجريبي. تُولَّد الجلسة تلقائياً وتُستبدل
426
- # مستقبلاً ببيانات "كود التعرف" الحقيقية.
427
- user_session = gr.State({"name": "", "email": "", "device_id": "", "rank": "free"})
428
  panel_visible_state = gr.State(False)
429
 
430
- # (3) بدل خيار vip/free النصي: ترحيب بالاسم + أيقونة ذهبية/فضية للرتبة
 
431
  with gr.Row():
432
- welcome_box = gr.HTML("<p role='status'>👋 جارٍ تجهيز جلستك...</p>")
433
- rank_icon_btn = gr.Button("🥈 FREE", elem_id="rank_icon_btn", scale=0, min_width=110)
434
 
435
- # الشاشة العامة لتفاصيل الاشتراك (مرتبطة بـ system/about_app.json الذي يكتبه المدير)
436
  subscription_panel = gr.HTML(visible=False)
437
 
438
- uploader = gr.File(label="اختر ملفاتك", file_count="multiple")
 
 
 
 
439
 
440
- # خيارات التحكم الإضافية
441
- gr.Markdown("### ⚙️ خيارات متقدمة (تؤثر على سرعة وجودة المعالجة)")
442
-
443
- with gr.Row():
444
- with gr.Column(scale=1):
445
  enable_table = gr.Checkbox(label="📊 كشف الجداول", value=False)
446
- gr.Markdown("<small style='color:red;'>⚠️ تفعيل كشف الجداول سيبطئ المعالجة حتى لو لم تكن هناك جداول.</small>")
447
- with gr.Column(scale=1):
448
  enable_handwriting = gr.Checkbox(label="🖐️ خط يدوي", value=False)
449
- gr.Markdown("<small style='color:red;'>⚠️ تفعيل الخط اليدوي يتطلب نموذجاً أثقل.</small>")
450
- with gr.Column(scale=1):
451
- enable_heavy_prep = gr.Checkbox(label="🔧 معالجة ثقيلة (منظور، ظلال)", value=False)
452
- gr.Markdown("<small style='color:red;'>⚠️ قد يزيد وقت المعالجة بشكل كبير.</small>")
453
- with gr.Column(scale=1):
454
  enable_high_dpi = gr.Checkbox(label="📐 دقة عالية (300 DPI)", value=False)
455
- gr.Markdown("<small style='color:blue;'>⚠️ مناسب للنصوص الصغيرة أو الممسوحة بدقة منخفضة.</small>")
456
- with gr.Column(scale=1):
457
  enable_fast = gr.Checkbox(label="⚡ الوضع السريع (EasyOCR)", value=False)
458
- gr.Markdown("<small style='color:green;'>🚀 يلغي جميع الخيارات الأخرى ويستخدم OCR خفيفاً.</small>")
459
-
460
- with gr.Row():
461
- with gr.Column(scale=1):
462
- enable_tashkeel = gr.Checkbox(label="🔤 تشكيل النص (حركات)", value=False)
463
- gr.Markdown("<small style='color:purple;'>🕋 يضيف حركات التشكيل (الفتحة، الضمة...) وقد يبطئ المعالجة.</small>")
464
 
465
- upload_btn = gr.Button("رفع الملفات", variant="primary")
466
- status_box = gr.HTML("<div>بانتظار رفع الملفات...</div>")
467
-
468
  gr.Markdown("### 📜 حالة التجميع والمعالجة الفورية")
469
  system_status = gr.HTML("<div role='status' aria-live='polite'>بانتظار تحديث الحالة من system/status.json...</div>")
470
 
471
- # تجهيز الجلسة (الترحيب + أيقونة الرتبة) عند فتح الصفحة
472
- upload_page.load(init_session, None, [user_session, welcome_box, rank_icon_btn])
 
 
 
 
473
 
474
- # فتح/إغلاق شاشة تفاصيل الاشتراك عند الضغط على أيقونة الرتبة
475
  rank_icon_btn.click(
476
  fn=toggle_subscription_panel,
477
  inputs=[user_session, panel_visible_state],
478
  outputs=[subscription_panel, panel_visible_state]
479
  )
480
 
481
- # ربط زر الرفع بالدالة المعدلة (تعتمد على الجلسة بدل مربع الاسم/الرتبة)
482
  upload_btn.click(
483
  fn=handle_upload,
484
- inputs=[uploader, user_session,
485
- enable_table, enable_handwriting,
486
- enable_heavy_prep, enable_high_dpi,
487
- enable_fast, enable_tashkeel],
488
  outputs=status_box
489
  ).then(read_status_json, None, system_status)
490
 
491
  status_timer = gr.Timer(3.0)
492
  status_timer.tick(read_status_json, None, system_status)
493
 
494
- # ------------------- قسم ملفاتي المُصحَّح -------------------
495
- with gr.Blocks(title="📁 ملفاتي") as files_page:
496
- gr.Markdown("<h1 style='text-align:center;'>📁 الملفات الجاهزة للتحويل</h1>")
497
- files_list = gr.Dropdown(label="اختر ملفاً من Final_Arabic_Files", choices=[], interactive=True)
498
- refresh_btn = gr.Button("تحديث القائمة")
499
-
500
- editor = gr.Textbox(label="مربع التعديل", lines=15)
501
- filename_box = gr.Textbox(label="اسم الملف (بدون امتداد)")
502
- ext_dropdown = gr.Dropdown(label="الامتداد", choices=["txt", "pdf", "docx", "xlsx", "csv", "html"], value="pdf")
503
- font_dropdown = gr.Dropdown(label="الخط", choices=["Arial", "Tahoma", "Amiri-Regular", "Cairo-Regular"], value="Amiri-Regular")
504
- font_size_dropdown = gr.Slider(label="حجم الخط", minimum=10, maximum=40, value=18, step=1)
505
-
506
- convert_btn = gr.Button("تحويل")
507
- download_file = gr.File(label="تحميل الملف")
508
- convert_status = gr.HTML()
509
-
510
- zip_btn = gr.Button("إنشاء ZIP")
511
- zip_file = gr.File(label="تحميل ZIP")
512
- zip_status = gr.HTML()
513
-
514
- def refresh_files():
515
- return gr.update(choices=list_final_files())
516
-
517
- files_page.load(refresh_files, None, files_list)
518
- refresh_btn.click(fn=refresh_files, outputs=files_list)
519
- files_list.change(
520
- fn=load_file_content,
521
- inputs=files_list,
522
- outputs=[editor, filename_box, files_list]
523
- )
524
- convert_btn.click(
525
- fn=convert_and_upload,
526
- inputs=[editor, filename_box, ext_dropdown, font_dropdown, font_size_dropdown],
527
- outputs=[convert_status, download_file]
528
- )
529
- zip_btn.click(
530
- fn=do_zip,
531
- inputs=[editor, filename_box],
532
- outputs=[zip_status, zip_file]
533
- )
534
-
535
- app = gr.TabbedInterface([upload_page, files_page], ["📤 الرفع المباشر", "📁 ملفاتي"])
536
-
537
  if __name__ == "__main__":
538
- app.launch(server_name="0.0.0.0", server_port=7860)
 
10
  from urllib.parse import unquote
11
 
12
  import gradio as gr
 
13
  from huggingface_hub import HfApi, list_repo_files, login, hf_hub_download
14
 
 
 
15
  # ============================================================
16
  # 0) إعدادات البيئة والنظام العامة
17
  # ============================================================
 
22
  os.makedirs("system", exist_ok=True)
23
 
24
  REPO_ID = "Asem75/aiocr_asistant"
 
25
  RAW_DIR = "RAW_FILES"
26
  WATCH_DIR_1 = "process"
27
  WATCH_DIR_2 = "process_files"
 
28
  FINAL_DIR = "Final_Arabic_Files"
29
  SYSTEM_STATUS_PATH = "system/status.json"
30
+ ABOUT_APP_PATH = "system/about_app.json"
31
+ MESSAGES_PATH = "system/messages.json"
32
+
33
+ # قاعدة بيانات الأجهزة الموثقة (ملف محلي لحفظ ربط البصمة بالاسم)
34
+ DEVICES_DB_PATH = "system/verified_devices.json"
35
 
36
  TOKEN = os.getenv("AIOCR_KEY")
37
  if not TOKEN:
 
57
  name = name.replace(" ", "_")
58
  return re.sub(r"[^\w\.\-]", "", name)
59
 
60
+ # ============================================================
61
+ # 🛡️ 2) كود الحارس الصارم (The Gatekeeper Middleware)
62
+ # ============================================================
63
+
64
+ def load_verified_devices():
65
+ """تحميل سجل الأجهزة الموثقة من المجلد المحلي"""
66
+ if os.path.exists(DEVICES_DB_PATH):
67
+ try:
68
+ with open(DEVICES_DB_PATH, "r", encoding="utf-8") as f:
69
+ return json.load(f)
70
+ except:
71
+ return {}
72
+ return {}
73
+
74
+ def save_device_to_db(username, email, token, rank="free"):
75
+ """تسجيل بيانات أول دخول للجهاز بشكل دائم وثابت"""
76
+ db = load_verified_devices()
77
+ db[token] = {
78
+ "username": username,
79
+ "email": email,
80
+ "rank": rank,
81
+ "first_seen": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
82
+ }
83
+ try:
84
+ with open(DEVICES_DB_PATH, "w", encoding="utf-8") as f:
85
+ json.dump(db, f, ensure_ascii=False, indent=2)
86
+ except Exception as e:
87
+ log_error(f"Failed to save device DB: {e}")
88
+
89
+ def process_gatekeeper_logic(url_params):
90
  """
91
+ 🧠 منطق الفحص الجنائي الصارم لمعلمات الرابط:
92
+ 1. غياب المعلمات المرفقة بالكامل -> توجيه للبوابة الأولى برابط متوافق سيادياً.
93
+ 2. ثبات الاسم للبصمة المتواجدة -> منع الدخول عند التلاعب بالاسم.
94
+ """
95
+ # تجهيز رابط العودة للبوابة السيادية الأولى بنفس النسق المتوافق مع المتصفحات وهواتف الجوال
96
+ # تم استخدام مساحة المساحة المحددة: Asem75/Divid_gait وتحويلها لرابط سبيس مباشر ومتوافق
97
+ fallback_url = "https://asem75-divid-gait.hf.space"
98
+ return_link_html = f"""
99
+ <div style='text-align:center; margin-top:15px;'>
100
+ <a href="{fallback_url}" target="_blank"
101
+ style="display:inline-block; padding:12px 25px; background:#d4af37; color:#0f172a; text-decoration:none; border-radius:8px; font-weight:bold; font-size:1.05rem; box-shadow:0 4px 15px rgba(212, 175, 55, 0.4);">
102
+ 🔄 العودة إلى بوابة الفحص والتوثيق السيادي
103
+ </a>
104
+ </div>
105
  """
 
106
 
107
+ if not url_params or not isinstance(url_params, dict):
108
+ return {
109
+ "allowed": False,
110
+ "msg": f"❌ وصول مرفوض: بروتوكول التحقق الثلاثي غائب تماماً. يرجى العودة للبوابة الأولى لسحب البصمة.<br>{return_link_html}",
111
+ "session": None
112
+ }
113
+
114
+ username = url_params.get("user", "").strip()
115
+ email = url_params.get("email", "").strip()
116
+ device_token = url_params.get("device_token", "").strip()
117
+
118
+ # 🛑 [حالة المنع 1]: إذا كانت البيانات ناقصة (الاسم أو الإيميل أو البصمة)
119
+ if not device_token or not username or not email:
120
+ log_error(f"PROTOCOL_FAILED: Missing Parameters. User: {username}, Email: {email}, Token: {device_token}")
121
+ return {
122
+ "allowed": False,
123
+ "msg": f"❌ فشل في بيانات التسجيل: بعض معلمات الهوية ناقصة أو غير مقروءة.<br>{return_link_html}",
124
+ "session": None
125
+ }
126
+
127
+ # 🛑 [حالة المنع 2]: محاولة تزوير بنية التوكن الرقمي
128
+ if not (device_token.startswith("SECURE-FPR-") or device_token.startswith("SECURE-FALLBACK-")):
129
+ log_error(f"FORGERY_DETECTED: Invalid token format: {device_token} from User: {username}")
130
+ return {
131
+ "allowed": False,
132
+ "msg": "❌ تم رصد بصمة غير مطابقة لمعايير التشفير السيادية (Signature Mismatch). الدخول محظور.",
133
+ "session": None
134
+ }
135
+
136
+ devices_db = load_verified_devices()
137
+
138
+ # 🔍 فحص البصمة في السجل التاريخي للنظام
139
+ if device_token in devices_db:
140
+ saved_data = devices_db[device_token]
141
+
142
+ # 🛑 [حالة المنع الصارم 3]: إذا حاول نفس الجهاز الدخول باسم مختلف عن أول تسجيل له
143
+ if saved_data.get("username") != username or saved_data.get("email") != email:
144
+ log_error(f"ACCESS_DENIED_NAME_MISMATCH: Device {device_token} tried changing name from {saved_data.get('username')} to {username}")
145
+ return {
146
+ "allowed": False,
147
+ "msg": "❌ خطأ في بيانات التسجيل: هذا الحساب وبصمة هذا الجهاز مسجلان لشخص آخر ولا يمكن تغيير البيانات.",
148
+ "session": None
149
+ }
150
+
151
+ # إذا تطابقت البصمة والاسم والإيميل تماماً مع السجل التاريخي
152
+ rank = saved_data.get("rank", "free")
153
+ status_msg = "🔄 تم التوثيق بنجاح! مرحباً بعودتك السيادية المحصنة."
154
+ else:
155
+ # جهاز جديد تماماً يدخل لأول مرة بالاسم الحالي: يتم قفله عليه للأبد
156
+ rank = "free"
157
+ save_device_to_db(username, email, device_token, rank)
158
+ status_msg = "✨ تم تسجيل بصمة جهازك وقفل الحساب على بياناتك الحالية بنجاح."
159
+
160
+ # بناء الجلسة الحقيقية الموثقة للمطابقة اللاحقة أثناء عمليات الرفع
161
+ session_data = {
162
+ "name": username,
163
+ "email": email,
164
+ "device_id": device_token,
165
+ "rank": rank,
166
+ "authenticated": True
167
+ }
168
+
169
+ return {
170
+ "allowed": True,
171
+ "msg": status_msg,
172
+ "session": session_data
173
+ }
174
+
175
+ # ============================================================
176
+ # 3) بقية دوال الواجهة (مع دالة قراءة الحالة المصححة تماماً)
177
+ # ============================================================
178
+
179
  def update_status_json(status_text: str):
180
  try:
181
  local_path = os.path.join("system", "status.json")
182
+ data = {"status": status_text, "updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
 
 
 
183
  with open(local_path, "w", encoding="utf-8") as f:
184
  json.dump(data, f, ensure_ascii=False, indent=2)
185
+ api.upload_file(path_or_fileobj=local_path, path_in_repo=SYSTEM_STATUS_PATH, repo_id=REPO_ID, repo_type="dataset", token=TOKEN)
186
+ except Exception as e:
 
 
 
 
 
 
 
187
  log_error(f"STATUS UPDATE ERROR: {e}")
188
 
 
 
 
189
  def send_report_to_manager(report_text: str):
190
  try:
191
  try:
192
+ local_existing = hf_hub_download(repo_id=REPO_ID, filename=MESSAGES_PATH, repo_type="dataset", token=TOKEN)
193
+ with open(local_existing, "r", encoding="utf-8") as f:
 
 
 
 
 
194
  messages = json.load(f)
195
+ except:
 
 
196
  messages = []
197
+ messages.append({"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "from": "upload_interface", "report": report_text})
 
 
 
 
 
 
198
  local_path = os.path.join("system", "messages.json")
199
+ with open(local_path, "w", encoding="utf-8") as f:
200
  json.dump(messages, f, ensure_ascii=False, indent=2)
201
+ api.upload_file(path_or_fileobj=local_path, path_in_repo=MESSAGES_PATH, repo_id=REPO_ID, repo_type="dataset", token=TOKEN)
202
+ except Exception as e:
 
 
 
 
 
 
 
203
  log_error(f"REPORT SEND ERROR: {e}")
204
 
205
+ def read_status_json():
206
+ """
207
+ دالة قراءة الحالة المصححة باستخدام hf_hub_download
208
+ لحذف خطأ 'HfApi' object has no attribute 'download_file'
209
+ """
210
  try:
211
  local_path = hf_hub_download(
212
  repo_id=REPO_ID,
213
+ filename=SYSTEM_STATUS_PATH,
214
  repo_type="dataset",
215
  token=TOKEN,
216
+ force_download=True
217
  )
218
  with open(local_path, "r", encoding="utf-8") as f:
219
  data = json.load(f)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
  status_text = data.get('status', 'غير معروف')
222
  updated_text = data.get('updated', 'غير معروف')
223
 
 
 
224
  html = f"""
225
  <div role="status" aria-live="polite" aria-atomic="true"
226
  style="font-size:16px; background-color: #f0f7ff; padding: 10px; border-radius: 5px; border: 1px solid #b3d7ff;">
 
231
  return html
232
  except Exception as e:
233
  log_error(f"STATUS READ ERROR: {e}")
234
+ return f"""<div role="status" aria-live="polite" style='color:orange; font-weight:bold;'>⚠️ لا يمكن قراءة ملف status.json حالياً — {e}</div>"""
235
 
236
+ def load_about_app(rank: str = "free") -> str:
237
+ badge = "🥇 عضوية VIP" if rank == "vip" else "🥈 عضوية مجانية (Free)"
238
+ return f'<div role="dialog" style="background:#ffffff; border:1px solid #ddd; border-radius:8px; padding:16px;"><h3>{badge}</h3><p>محتوى وتفاصيل الاشتراك المعتمد للرتبة الحالية الفعالة.</p></div>'
239
+
240
+ def check_free_user_limits(username):
241
+ return True, "مسموح بالرفع"
242
 
243
+ def handle_upload(files, session, enable_table, enable_handwriting, enable_heavy_prep, enable_high_dpi, enable_fast, enable_tashkeel):
244
+ if not session or not session.get("authenticated"):
245
+ return "<div role='alert' style='color:red;'>❌ خطأ أمني صارم: الجلسة غير موثقة، لا يمكنك رفع أي ملف!</div>"
246
+
247
+ if not files:
248
  return "<div role='alert'>❌ يرجى اختيار ملفات للرفع</div>"
249
 
250
+ username = sanitize_filename(session.get("name", "Unknown"))
 
 
 
251
  rank = session.get("rank", "free")
252
  is_vip = (rank == "vip")
253
 
254
  if not is_vip:
255
  allowed, message = check_free_user_limits(username)
256
+ if not allowed:
257
+ return f"<div role='alert' style='color:red;'>{message}</div>"
258
 
259
  success = 0
260
  prefix = f"VIP_{username}_" if is_vip else f"{username}_"
 
262
  for file in files:
263
  try:
264
  fname = sanitize_filename(unquote(os.path.basename(file.name)))
265
+ option_prefix = "fast_" if enable_fast else ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  final_filename = f"{prefix}{option_prefix}{fname}"
 
 
 
 
 
 
 
 
 
267
 
268
+ api.upload_file(path_or_fileobj=file.name, path_in_repo=f"{RAW_DIR}/{final_filename}", repo_id=REPO_ID, repo_type="dataset", token=TOKEN)
 
 
 
 
269
  success += 1
270
+ except Exception as e:
271
+ log_error(f"UPLOAD ERROR: {e}")
272
 
 
 
 
 
273
  if success > 0:
274
+ update_status_json(f"تم رفع {success} ملفات بواسطة {username}")
275
+ send_report_to_manager(f"المستخدم {username} ({rank}) رفع {success} ملف/ات بنجاح.")
276
+ return f"<div role='status' style='color:green;'>تم رفع {success} ملف/ات بنجاح ✔</div>"
 
 
 
 
 
277
 
278
  # ============================================================
279
+ # ⚙️ 4) دالة حقن الجلسة والتحكم بعناصر العرض
280
  # ============================================================
281
 
282
+ def gatekeeper_init_session(request: gr.Request):
283
+ """
284
+ 🚀 فحص فوري وصارم عند تحميل صفحة بايثون لمنع المتلاعبين
285
+ وإلغاء الجلسات الافتراضية العشوائية (لا وجود لزائر).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  """
287
+ params = dict(request.query_params)
288
+ result = process_gatekeeper_logic(params)
289
+
290
+ if not result["allowed"]:
291
+ # حجب الواجهة وإظهار رسالة المنع مع الرابط المتوافق للعودة لبوابة الفحص
292
+ return (
293
+ {"authenticated": False},
294
+ result["msg"],
295
+ gr.update(visible=False),
296
+ gr.update(visible=False),
297
+ gr.update(visible=False)
298
+ )
299
+
300
+ sess = result["session"]
301
+ welcome_html = f"""
302
+ <div style='background: #f0fdf4; border: 1px solid #bbf7d0; padding: 12px; border-radius: 8px;'>
303
+ <p role='status' style='font-size:16px; margin:0; color:#166534;'>👋 {result['msg']} | الحساب الموثق: <b>{sess['name']}</b></p>
304
+ <p style='font-size:12px; color:#666; margin:4px 0 0 0;'>🆔 بصمة العتاد المتطابقة: <code>{sess['device_id']}</code></p>
305
+ </div>
306
  """
307
+ icon_label = "🥇 VIP" if sess["rank"] == "vip" else "🥈 FREE"
308
+
309
+ return (
310
+ sess,
311
+ welcome_html,
312
+ gr.update(value=icon_label, visible=True),
313
+ gr.update(visible=True),
314
+ gr.update(visible=True)
315
+ )
316
 
317
  def toggle_subscription_panel(session, is_visible):
318
+ if not session or not session.get("authenticated"):
319
+ return gr.update(visible=False), is_visible
320
  new_visible = not is_visible
321
  if new_visible:
322
+ return gr.update(value=load_about_app(session.get("rank", "free")), visible=True), new_visible
 
323
  return gr.update(visible=False), new_visible
324
 
325
  # ============================================================
326
+ # 5) بناء الواجهة الرادارية الرسمية المحصنة
327
  # ============================================================
328
 
329
  with gr.Blocks(title="📤 بوابة الرفع") as upload_page:
330
+ gr.Markdown("<h1 style='text-align:center;'>📤 بوابة الرفع ودورة حياة الملفات الموثقة</h1>")
331
 
332
+ # تهيئة حالة أمنية صارمة فارغة لحين الفحص (تم نسف الجلسات الافتراضية العشوائية للزوار)
333
+ user_session = gr.State({"authenticated": False})
 
334
  panel_visible_state = gr.State(False)
335
 
336
+ welcome_box = gr.HTML("<p role='status'>🛡️ جاري مطابقة بصمة العتاد التاريخية والتحقق من سلامة البيانات السيادية...</p>")
337
+
338
  with gr.Row():
339
+ rank_icon_btn = gr.Button("🥈 FREE", elem_id="rank_icon_btn", scale=0, min_width=110, visible=False)
 
340
 
 
341
  subscription_panel = gr.HTML(visible=False)
342
 
343
+ # المجموعات تخضع للإخفاء الكامل الفوري في حال حدوث خطأ أو تلاعب بالاسم
344
+ with gr.Group() as upload_area:
345
+ uploader = gr.File(label="اختر ملفاتك", file_count="multiple")
346
+ upload_btn = gr.Button("رفع الملفات", variant="primary")
347
+ status_box = gr.HTML("<div>بانتظار رفع الملفات...</div>")
348
 
349
+ with gr.Group() as options_area:
350
+ gr.Markdown("### ⚙️ خيارات معالجة العتاد المتقدمة")
351
+ with gr.Row():
 
 
352
  enable_table = gr.Checkbox(label="📊 كشف الجداول", value=False)
 
 
353
  enable_handwriting = gr.Checkbox(label="🖐️ خط يدوي", value=False)
354
+ enable_heavy_prep = gr.Checkbox(label="🔧 معالجة ثقيلة", value=False)
 
 
 
 
355
  enable_high_dpi = gr.Checkbox(label="📐 دقة عالية (300 DPI)", value=False)
 
 
356
  enable_fast = gr.Checkbox(label="⚡ الوضع السريع (EasyOCR)", value=False)
357
+ enable_tashkeel = gr.Checkbox(label="🔤 تشكيل النص", value=False)
 
 
 
 
 
358
 
 
 
 
359
  gr.Markdown("### 📜 حالة التجميع والمعالجة الفورية")
360
  system_status = gr.HTML("<div role='status' aria-live='polite'>بانتظار تحديث الحالة من system/status.json...</div>")
361
 
362
+ # استدعاء الحارس فور شحن الصفحة بالمتصفح
363
+ upload_page.load(
364
+ fn=gatekeeper_init_session,
365
+ inputs=None,
366
+ outputs=[user_session, welcome_box, rank_icon_btn, upload_area, options_area]
367
+ )
368
 
 
369
  rank_icon_btn.click(
370
  fn=toggle_subscription_panel,
371
  inputs=[user_session, panel_visible_state],
372
  outputs=[subscription_panel, panel_visible_state]
373
  )
374
 
 
375
  upload_btn.click(
376
  fn=handle_upload,
377
+ inputs=[uploader, user_session, enable_table, enable_handwriting, enable_heavy_prep, enable_high_dpi, enable_fast, enable_tashkeel],
 
 
 
378
  outputs=status_box
379
  ).then(read_status_json, None, system_status)
380
 
381
  status_timer = gr.Timer(3.0)
382
  status_timer.tick(read_status_json, None, system_status)
383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  if __name__ == "__main__":
385
+ upload_page.launch()