Spaces:
Paused
Paused
Update app.py
Browse files
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
"""
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
"""
|
| 69 |
-
return "زائر_" + secrets.token_hex(3)
|
| 70 |
|
| 71 |
-
|
| 72 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
try:
|
| 139 |
local_path = hf_hub_download(
|
| 140 |
repo_id=REPO_ID,
|
| 141 |
-
filename=
|
| 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 |
-
|
| 256 |
-
|
|
|
|
|
|
|
|
|
|
| 257 |
|
| 258 |
-
def handle_upload(files, session,
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
if not files:
|
| 263 |
return "<div role='alert'>❌ يرجى اختيار ملفات للرفع</div>"
|
| 264 |
|
| 265 |
-
|
| 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;
|
| 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 |
-
|
| 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 |
-
|
| 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(
|
| 328 |
-
|
| 329 |
-
|
| 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 |
-
#
|
| 338 |
# ============================================================
|
| 339 |
|
| 340 |
-
def
|
| 341 |
-
"""
|
| 342 |
-
|
| 343 |
-
|
| 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 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
"""
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
|
| 410 |
def toggle_subscription_panel(session, is_visible):
|
| 411 |
-
|
|
|
|
| 412 |
new_visible = not is_visible
|
| 413 |
if new_visible:
|
| 414 |
-
|
| 415 |
-
return gr.update(value=load_about_app(rank), visible=True), new_visible
|
| 416 |
return gr.update(visible=False), new_visible
|
| 417 |
|
| 418 |
# ============================================================
|
| 419 |
-
#
|
| 420 |
# ============================================================
|
| 421 |
|
| 422 |
with gr.Blocks(title="📤 بوابة الرفع") as upload_page:
|
| 423 |
-
gr.Markdown("<h1 style='text-align:center;'>📤 بوابة الرفع ودورة حياة الملفات</h1>")
|
| 424 |
|
| 425 |
-
#
|
| 426 |
-
|
| 427 |
-
user_session = gr.State({"name": "", "email": "", "device_id": "", "rank": "free"})
|
| 428 |
panel_visible_state = gr.State(False)
|
| 429 |
|
| 430 |
-
|
|
|
|
| 431 |
with gr.Row():
|
| 432 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
|
| 440 |
-
|
| 441 |
-
|
| 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.
|
| 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.
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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()
|