Spaces:
Paused
Paused
| import gradio as gr | |
| import os | |
| import re | |
| import tempfile | |
| import zipfile | |
| from huggingface_hub import HfApi, hf_hub_download, login | |
| # مكتبات تحويل الصيغ | |
| from fpdf import FPDF | |
| from docx import Document | |
| from docx.shared import Pt | |
| from docx.enum.text import WD_ALIGN_PARAGRAPH | |
| import arabic_reshaper | |
| from bidi.algorithm import get_display | |
| import openpyxl | |
| try: | |
| from ebooklib import epub | |
| EPUB_AVAILABLE = True | |
| except ImportError: | |
| EPUB_AVAILABLE = False | |
| # ================================================================= | |
| # إعدادات النظام العامة – مطابقة كاملة 100% مع الطبقة الثانية | |
| # ================================================================= | |
| # جلب التوكن بالكبير تماماً كما هو معرف في السبيس الآخر ومعالجته فوراً | |
| TOKEN = os.getenv("AIOCR_KEY") | |
| if TOKEN: | |
| # تنظيف التوكن تلقائياً من أي زوائد أو أسطر خفية قد تنتج عن اللصق | |
| TOKEN = TOKEN.strip().split()[0] | |
| try: | |
| login(token=TOKEN) | |
| api = HfApi(token=TOKEN) | |
| except Exception as login_err: | |
| print(f"⚠️ تنبيه تسجيل الدخول: {login_err}") | |
| api = HfApi() | |
| else: | |
| api = HfApi() | |
| REPO_ID = "Asem75/aiocr_asistant" | |
| FINAL_DIR = "Final_Arabic_Files" | |
| MAIN_PORTAL = "https://huggingface.co/spaces/Asem75/aiocr" | |
| # تصميم موحد ونظيف بدون أي تداخل برمجي | |
| custom_css = """ | |
| body, .gradio-container { | |
| background-color: #0a0a0a !important; | |
| color: #e0c080 !important; | |
| } | |
| .gr-box, .gr-form, .gr-panel { | |
| background-color: #1a1a1a !important; | |
| border: 1px solid #c8a44c !important; | |
| } | |
| .gr-button { | |
| background-color: #2a2a2a !important; | |
| color: #e0c080 !important; | |
| border: 1px solid #c8a44c !important; | |
| } | |
| .gr-button:hover { | |
| background-color: #3d2e1a !important; | |
| } | |
| .gr-textbox, .gr-dropdown { | |
| background-color: #121212 !important; | |
| color: #e0c080 !important; | |
| border: 1px solid #c8a44c !important; | |
| } | |
| #golden-editor textarea { | |
| background-color: #121212 !important; | |
| color: #f5d78c !important; | |
| font-size: 16px; | |
| font-family: 'Courier New', monospace; | |
| line-height: 1.6; | |
| direction: rtl; | |
| } | |
| .warning-banner { | |
| background-color: #3d2e1a; | |
| color: #ffcc00; | |
| padding: 8px; | |
| border-radius: 4px; | |
| margin-bottom: 10px; | |
| text-align: center; | |
| font-weight: bold; | |
| } | |
| .error-box { | |
| background-color: #4a1a1a; | |
| color: #ff9999; | |
| padding: 10px; | |
| border: 1px solid #ff3333; | |
| border-radius: 4px; | |
| font-size: 13px; | |
| margin: 5px 0; | |
| } | |
| """ | |
| def clean_filename(fn: str) -> str: | |
| patterns = [r'^Final_Result_', r'^OCR_Result_', r'^Processed_', r'^Result_', r'^Final_'] | |
| for pat in patterns: | |
| fn = re.sub(pat, '', fn, flags=re.IGNORECASE) | |
| name, _ = os.path.splitext(fn) | |
| return name.strip() | |
| # ------------------------------- | |
| # الرادار الذكي الموحد لقراءة الملفات | |
| # ------------------------------- | |
| def list_archive_files_reporting(): | |
| if not TOKEN: | |
| return None, "<div class='error-box'>🔒 المتغير البيئي AIOCR_KEY غير موجود في إعدادات السبيس الحالي. يرجى إضافته في قسم Settings بالأحرف الكبيرة.</div>" | |
| try: | |
| # استخدام الكائن الموثق بـ TOKEN المماثل للطبقة الثانية | |
| files = api.list_repo_files(REPO_ID, repo_type="dataset") | |
| results = [] | |
| prefix_folder = FINAL_DIR + "/" | |
| for f in files: | |
| if f.endswith('/') or f.endswith('.gitkeep') or '.git' in f: | |
| continue | |
| if f.lower().startswith(prefix_folder.lower()): | |
| # قص اسم المجلد للحصول على اسم الملف الصافي | |
| results.append(f[len(prefix_folder):]) | |
| elif '/' not in f: | |
| results.append(f) | |
| results = sorted(list(set(results))) | |
| if not results: | |
| return [], f"<p style='color:#ffd700; text-align:center; padding:10px;'>🗄️ الرادار متصل بالخزنة، ولكن المجلد الموحد '{FINAL_DIR}' فارغ حالياً ولا يحتوي ملفات مستخرجة.</p>" | |
| return results, None | |
| except Exception as e: | |
| error_msg = str(e) | |
| return None, f"<div class='error-box'>⚠️ الرادار لم يتمكن من مسح المستودع الخاص.<br>الخطأ المباشر: {error_msg}</div>" | |
| def get_preview(filename: str) -> str: | |
| if not filename or not TOKEN: return "" | |
| try: | |
| try: | |
| path = hf_hub_download(repo_id=REPO_ID, filename=f"{FINAL_DIR}/{filename}", repo_type="dataset", token=TOKEN) | |
| except: | |
| path = hf_hub_download(repo_id=REPO_ID, filename=filename, repo_type="dataset", token=TOKEN) | |
| with open(path, 'r', encoding='utf-8') as f: | |
| lines = [f.readline() for _ in range(5)] | |
| return "".join(lines) | |
| except Exception as e: | |
| return f"⚠️ خطأ في المعاينة: {e}" | |
| def load_file_content(filename: str): | |
| if not filename or not TOKEN: return "", "" | |
| try: | |
| try: | |
| path = hf_hub_download(repo_id=REPO_ID, filename=f"{FINAL_DIR}/{filename}", repo_type="dataset", token=TOKEN) | |
| except: | |
| path = hf_hub_download(repo_id=REPO_ID, filename=filename, repo_type="dataset", token=TOKEN) | |
| with open(path, 'r', encoding='utf-8') as f: | |
| content = f.read() | |
| return content, clean_filename(filename) | |
| except Exception as e: | |
| return f"⚠️ خطأ في تحميل الملف: {e}", filename | |
| # ---- أدوات معالجة النصوص المحررة ---- | |
| def clean_spaces(text): | |
| text = re.sub(r'[ \t]+', ' ', text) | |
| text = re.sub(r'\n\s*\n', '\n\n', text) | |
| return text.strip() | |
| def rtl_align(text): return '\u202B' + text + '\u202C' | |
| def convert_numerals(text): | |
| eastern = '٠١٢٣٤٥٦٧٨٩' | |
| western = '0123456789' | |
| return text.translate(str.maketrans(western, eastern)) | |
| # ---- محركات تحويل وتصدير المستندات ---- | |
| def convert_to_pdf(text, filename): | |
| try: | |
| reshaped = arabic_reshaper.reshape(text) | |
| bidi_text = get_display(reshaped) | |
| except Exception: bidi_text = text | |
| pdf = FPDF() | |
| pdf.add_page() | |
| font_path = "/usr/share/fonts/truetype/noto/NotoNaskhArabic-Regular.ttf" | |
| if not os.path.exists(font_path): font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" | |
| if os.path.exists(font_path): | |
| pdf.add_font("Arabic", "", font_path, uni=True) | |
| pdf.set_font("Arabic", size=12) | |
| else: pdf.set_font("Helvetica", size=12) | |
| pdf.set_auto_page_break(auto=True, margin=15) | |
| pdf.multi_cell(0, 10, bidi_text) | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") | |
| pdf.output(tmp.name) | |
| return tmp.name | |
| def convert_to_docx(text, filename): | |
| doc = Document() | |
| for line in text.split('\n'): | |
| p = doc.add_paragraph() | |
| run = p.add_run(line) | |
| run.font.size = Pt(12) | |
| run.font.rtl = True | |
| p.alignment = WD_ALIGN_PARAGRAPH.RIGHT | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".docx") | |
| doc.save(tmp.name) | |
| return tmp.name | |
| def convert_to_rtf(text, filename): | |
| rtf = r"{\rtf1\ansi\ansicpg1256\deff0\nouicompat" | |
| rtf += r"{\fonttbl{\f0\fnil\fcharset0 Arial;}}" | |
| rtf += r"\viewkind4\uc1\pard\rtlpar\f0\fs24 " | |
| rtf += text.replace('\n', r'\par ') + r"}" | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".rtf") | |
| with open(tmp.name, 'w', encoding='utf-8') as f: f.write(rtf) | |
| return tmp.name | |
| def convert_to_html(text, filename): | |
| html = f"""<html><head><meta charset="utf-8"><title>{filename}</title></head> | |
| <body dir="rtl" style="font-family: Arial; padding:20px;"> | |
| <pre style="white-space: pre-wrap;">{text}</pre> | |
| </body></html>""" | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".html") | |
| with open(tmp.name, 'w', encoding='utf-8') as f: f.write(html) | |
| return tmp.name | |
| def convert_to_xlsx(text, filename): | |
| wb = openpyxl.Workbook() | |
| ws = wb.active | |
| ws.title = "AIOCR Output" | |
| for i, line in enumerate(text.split('\n'), start=1): | |
| ws.cell(row=i, column=1, value=line) | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") | |
| wb.save(tmp.name) | |
| return tmp.name | |
| def convert_to_epub(text, filename): | |
| if not EPUB_AVAILABLE: raise gr.Error("مكتبة ebooklib غير مثبتة.") | |
| book = epub.EpubBook() | |
| book.set_identifier('aiocr_output') | |
| book.set_title(filename) | |
| book.set_language('ar') | |
| chapter = epub.EpubHtml(title='Output', file_name='chap_1.xhtml', lang='ar') | |
| chapter.content = f'<html><head></head><body dir="rtl"><pre>{text}</pre></body></html>' | |
| book.add_item(chapter) | |
| book.toc = [epub.Link('chap_1.xhtml', 'Output', 'output')] | |
| book.spine = [chapter] | |
| book.add_item(epub.EpubNcx()) | |
| book.add_item(epub.EpubNav()) | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".epub") | |
| epub.write_epub(tmp.name, book, {}) | |
| return tmp.name | |
| def convert_to_code(text, filename): | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".txt") | |
| with open(tmp.name, 'w', encoding='utf-8') as f: f.write(text) | |
| return tmp.name | |
| def render_interface(): | |
| files_list, error_html = list_archive_files_reporting() | |
| if error_html: | |
| return error_html | |
| html = '<div style="max-height:400px; overflow-y:auto;">' | |
| for f in files_list: | |
| safe = f.replace("'", "\\'") | |
| html += f""" | |
| <div style="cursor:pointer; padding:8px; border-bottom:1px solid #333; color:#ffd700;" | |
| onclick="selectFile('{safe}')" | |
| ondblclick="loadFile('{safe}')"> | |
| 📄 {f} | |
| </div>""" | |
| html += '</div>' | |
| html += """ | |
| <script> | |
| function selectFile(filename) { | |
| var inp = document.querySelector('#preview_filename textarea, #preview_filename input'); | |
| if(inp) { inp.value = filename; inp.dispatchEvent(new Event('input', {bubbles:true})); } | |
| } | |
| function loadFile(filename) { | |
| var inp = document.querySelector('#load_filename textarea, #load_filename input'); | |
| if(inp) { inp.value = filename; inp.dispatchEvent(new Event('input', {bubbles:true})); | |
| var btn = document.querySelector('#load_btn button'); | |
| if(btn) btn.click(); | |
| } | |
| } | |
| </script> | |
| """ | |
| return html | |
| # ------------------------------- | |
| # واجهة مستخدم GRADIO المستقرة | |
| # ------------------------------- | |
| with gr.Blocks(css=custom_css, title="AIOCR_RESULT – المحرر الذهبي") as demo: | |
| current_file_state = gr.Textbox(visible=False) | |
| with gr.Column(visible=True) as main_ui: | |
| gr.HTML('<div class="warning-banner">📡 تم ربط الرادار بالخزنة بنظام فك تشفير المعرفات والمفاتيح السيادية الموحدة</div>') | |
| with gr.Row(): | |
| with gr.Column(scale=1, elem_classes="sidebar"): | |
| refresh_btn = gr.Button("🔄 تحديث ورصد الخزنة", size="sm") | |
| archive_html = gr.HTML(value="<p style='color:#c8a44c; text-align:center;'>جاري قراءة الخزنة الرقمية الموحدة...</p>") | |
| preview_box = gr.Textbox(label="📋 معاينة سريعة", lines=6, interactive=False) | |
| with gr.Column(scale=3): | |
| editor = gr.Textbox( | |
| label="📜 شاشة المحرر الذهبي الرئيسي", | |
| lines=20, | |
| elem_id="golden-editor", | |
| placeholder="انقر نقراً مزدوجاً (Double Click) على أي ملف للفتحه هنا بمجرد ظهوره بالأرشيف..." | |
| ) | |
| with gr.Row(): | |
| copy_btn = gr.Button("📋 نسخ النص") | |
| clean_btn = gr.Button("🧹 تنظيف المسافات") | |
| rtl_btn = gr.Button("↩️ محاذاة RTL") | |
| numeral_btn = gr.Button("🔢 أرقام عربية") | |
| with gr.Row(): | |
| filename_input = gr.Textbox(label="📝 اسم الملف النهائي", value="", scale=3) | |
| format_dropdown = gr.Dropdown( | |
| choices=["PDF", "DOCX", "TXT", "RTF", "HTML", "EXCEL (XLSX)", "EPUB", "CODE"], | |
| value="PDF", | |
| label="📄 تنسيق التصدير" | |
| ) | |
| with gr.Row(): | |
| download_single_btn = gr.DownloadButton("📥 تحميل المستند", variant="primary") | |
| download_zip_btn = gr.DownloadButton("🗜️ تحميل الأرشيف (ZIP)", variant="secondary") | |
| with gr.Row(): | |
| gr.HTML(f""" | |
| <div style="display:flex; justify-content:center; width:100%; margin-top:20px;"> | |
| <a href="{MAIN_PORTAL}" style="text-decoration:none; padding:12px 24px; background:#1a1a1a; color:#ffd700; border:2px solid #c8a44c; border-radius:8px; font-weight:bold;">📤 العودة لبوابة الرفع الرئيسية</a> | |
| </div> | |
| """) | |
| preview_filename = gr.Textbox(visible=False, elem_id="preview_filename") | |
| load_filename = gr.Textbox(visible=False, elem_id="load_filename") | |
| load_btn = gr.Button("تحميل", visible=False, elem_id="load_btn") | |
| timer = gr.Timer(12) | |
| demo.load(fn=render_interface, outputs=archive_html) | |
| refresh_btn.click(fn=render_interface, outputs=archive_html) | |
| timer.tick(fn=render_interface, outputs=archive_html) | |
| preview_filename.change(fn=get_preview, inputs=preview_filename, outputs=preview_box) | |
| def load_full(filename): | |
| text, clean_name = load_file_content(filename) | |
| return text, clean_name, filename | |
| load_btn.click(fn=load_full, inputs=load_filename, outputs=[editor, filename_input, current_file_state]) | |
| copy_btn.click( | |
| fn=None, inputs=None, outputs=None, | |
| js=""" | |
| () => { | |
| var txt = document.querySelector('#golden-editor textarea'); | |
| if(txt) navigator.clipboard.writeText(txt.value).then(() => alert('✔ تم نسخ النص!')); | |
| } | |
| """ | |
| ) | |
| clean_btn.click(fn=clean_spaces, inputs=editor, outputs=editor) | |
| rtl_btn.click(fn=rtl_align, inputs=editor, outputs=editor) | |
| numeral_btn.click(fn=convert_numerals, inputs=editor, outputs=editor) | |
| def handle_single_download(text, filename, fmt, current_file): | |
| if not text: raise gr.Error("لا يوجد نص.") | |
| format_map = { | |
| "PDF": (convert_to_pdf, ".pdf"), "DOCX": (convert_to_docx, ".docx"), | |
| "TXT": (lambda t, fn: tempfile.NamedTemporaryFile(delete=False, suffix=".txt").name, ".txt"), | |
| "RTF": (convert_to_rtf, ".rtf"), "HTML": (convert_to_html, ".html"), | |
| "EXCEL (XLSX)": (convert_to_xlsx, ".xlsx"), "EPUB": (convert_to_epub, ".epub"), "CODE": (convert_to_code, ".txt") | |
| } | |
| converter, ext = format_map.get(fmt, (lambda t, fn: tempfile.NamedTemporaryFile(delete=False, suffix=".txt").name, ".txt")) | |
| save_name = filename.strip() + ext | |
| if fmt == "TXT": | |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".txt") | |
| with open(tmp.name, 'w', encoding='utf-8') as f: f.write(text) | |
| filepath = tmp.name | |
| else: | |
| filepath = converter(text, save_name) | |
| return filepath | |
| download_single_btn.click(fn=handle_single_download, inputs=[editor, filename_input, format_dropdown, current_file_state], outputs=download_single_btn) | |
| def handle_zip_download(): | |
| files_list, _ = list_archive_files_reporting() | |
| if not files_list: raise gr.Error("لا توجد ملفات متوفرة.") | |
| zip_buffer = tempfile.NamedTemporaryFile(delete=False, suffix=".zip") | |
| with zipfile.ZipFile(zip_buffer, 'w') as zf: | |
| for fn in files_list: | |
| try: | |
| path = hf_hub_download(repo_id=REPO_ID, filename=f"{FINAL_DIR}/{fn}", repo_type="dataset", token=TOKEN) | |
| zf.write(path, arcname=fn) | |
| except: continue | |
| return zip_buffer.name | |
| download_zip_btn.click(fn=handle_zip_download, outputs=download_zip_btn) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0") | |