| |
| """ |
| وحدة تطبيق تحليل المستندات |
| |
| هذا الملف يحتوي على الفئة الرئيسية لتطبيق تحليل المستندات. |
| """ |
|
|
| |
| import os |
| import sys |
| import logging |
| import base64 |
| import json |
| import time |
| from io import BytesIO |
| from pathlib import Path |
| from urllib.parse import urlparse |
| from tempfile import NamedTemporaryFile |
|
|
| |
| import streamlit as st |
|
|
| |
| import requests |
| from PIL import Image |
|
|
| try: |
| |
| from docling_core.types.doc import ImageRefMode |
| from docling_core.types.doc.document import DocTagsDocument, DoclingDocument |
| from mlx_vlm import load, generate |
| from mlx_vlm.prompt_utils import apply_chat_template |
| from mlx_vlm.utils import load_config, stream_generate |
| docling_available = True |
| except ImportError: |
| docling_available = False |
| logging.warning("لم يتم العثور على مكتبات Docling و MLX VLM. بعض الوظائف قد لا تعمل.") |
|
|
| try: |
| |
| from pdf2image import convert_from_path |
| pdf_conversion_available = True |
| except ImportError: |
| pdf_conversion_available = False |
| logging.warning("لم يتم العثور على مكتبة pdf2image. لن يمكن تحويل ملفات PDF إلى صور.") |
|
|
| |
| current_dir = os.path.dirname(os.path.abspath(__file__)) |
| parent_dir = os.path.dirname(os.path.dirname(current_dir)) |
| if parent_dir not in sys.path: |
| sys.path.append(parent_dir) |
|
|
| |
| try: |
| |
| from .services.text_extractor import TextExtractor |
| from .services.item_extractor import ItemExtractor |
| from .services.document_parser import DocumentParser |
| except ImportError: |
| try: |
| |
| from modules.document_analysis.services.text_extractor import TextExtractor |
| from modules.document_analysis.services.item_extractor import ItemExtractor |
| from modules.document_analysis.services.document_parser import DocumentParser |
| except ImportError: |
| |
| logging.warning("لا يمكن استيراد خدمات تحليل المستندات. استخدام التعريفات المؤقتة.") |
| |
| class TextExtractor: |
| def __init__(self, config=None): |
| self.config = config or {} |
| |
| def extract_from_pdf(self, file_path): |
| return "نص مستخرج مؤقت من PDF" |
| |
| def extract_from_docx(self, file_path): |
| return "نص مستخرج مؤقت من DOCX" |
| |
| def extract_from_image(self, file_path): |
| return "نص مستخرج مؤقت من صورة" |
| |
| def extract(self, file_path): |
| _, ext = os.path.splitext(file_path) |
| ext = ext.lower() |
| |
| if ext == '.pdf': |
| return self.extract_from_pdf(file_path) |
| elif ext in ('.doc', '.docx'): |
| return self.extract_from_docx(file_path) |
| elif ext in ('.jpg', '.jpeg', '.png'): |
| return self.extract_from_image(file_path) |
| else: |
| return "نوع ملف غير مدعوم" |
| |
| class ItemExtractor: |
| def __init__(self, config=None): |
| self.config = config or {} |
| |
| def extract_tables(self, document): |
| return [{"عنوان": "جدول مؤقت", "بيانات": []}] |
| |
| def extract(self, file_path): |
| return [ |
| {"بند": "بند مؤقت 1", "قيمة": 1000}, |
| {"بند": "بند مؤقت 2", "قيمة": 2000}, |
| {"بند": "بند مؤقت 3", "قيمة": 3000} |
| ] |
| |
| class DocumentParser: |
| def __init__(self, config=None): |
| self.config = config or {} |
| |
| def parse_document(self, file_path): |
| return {"نوع": "مستند مؤقت", "محتوى": "محتوى مؤقت"} |
| |
| def parse(self, file_path): |
| return { |
| "نوع المستند": "مستند مؤقت", |
| "عدد الصفحات": 5, |
| "تاريخ التحليل": "2025-03-24", |
| "درجة الثقة": "80%", |
| "ملاحظات": "تحليل مؤقت للمستند" |
| } |
|
|
|
|
| class DoclingAnalyzer: |
| """ |
| فئة لتحليل المستندات باستخدام نماذج Docling و MLX VLM |
| """ |
| def __init__(self): |
| self.model = None |
| self.processor = None |
| self.config = None |
| self.docling_available = False |
| |
| try: |
| |
| import os |
| from mlx_vlm import load, generate |
| from mlx_vlm.utils import load_config |
| |
| model_path = "ds4sd/SmolDocling-256M-preview-mlx-bf16" |
| self.model, self.processor = load(model_path) |
| self.config = load_config(model_path) |
| self.docling_available = True |
| except Exception as e: |
| print(f"خطأ في تحميل نموذج Docling: {str(e)}") |
| self.docling_available = False |
| |
| def is_available(self): |
| """التحقق من توفر نماذج Docling""" |
| return self.docling_available and self.model is not None |
| |
| def analyze_image(self, image_path=None, image_url=None, image_bytes=None, prompt="Convert this page to docling."): |
| """ |
| تحليل صورة باستخدام نموذج Docling |
| |
| المعلمات: |
| image_path (str): مسار الصورة المحلية (اختياري) |
| image_url (str): رابط الصورة (اختياري) |
| image_bytes (bytes): بيانات الصورة (اختياري) |
| prompt (str): التوجيه للنموذج |
| |
| العوائد: |
| dict: نتائج التحليل متضمنة النص والعلامات والمستند |
| """ |
| if not self.is_available(): |
| return { |
| "error": "Docling غير متوفر. يرجى تثبيت المكتبات المطلوبة." |
| } |
| |
| try: |
| from io import BytesIO |
| from pathlib import Path |
| from urllib.parse import urlparse |
| import requests |
| from PIL import Image |
| from docling_core.types.doc import ImageRefMode |
| from docling_core.types.doc.document import DocTagsDocument, DoclingDocument |
| from mlx_vlm.prompt_utils import apply_chat_template |
| from mlx_vlm.utils import stream_generate, load_image |
| |
| |
| pil_image = None |
| image_source = None |
| |
| if image_url: |
| try: |
| response = requests.get(image_url, stream=True, timeout=10) |
| response.raise_for_status() |
| pil_image = Image.open(BytesIO(response.content)) |
| image_source = image_url |
| except Exception as e: |
| return {"error": f"فشل في تحميل الصورة من الرابط: {str(e)}"} |
| elif image_path: |
| try: |
| |
| if not Path(image_path).exists(): |
| return {"error": f"ملف الصورة غير موجود: {image_path}"} |
| pil_image = Image.open(image_path) |
| image_source = image_path |
| except Exception as e: |
| return {"error": f"فشل في فتح ملف الصورة: {str(e)}"} |
| elif image_bytes: |
| try: |
| pil_image = Image.open(BytesIO(image_bytes)) |
| |
| temp_path = "/tmp/temp_image.jpg" |
| pil_image.save(temp_path) |
| image_source = temp_path |
| except Exception as e: |
| return {"error": f"فشل في معالجة بيانات الصورة: {str(e)}"} |
| else: |
| return {"error": "يجب توفير مصدر للصورة (مسار، رابط، أو بيانات)"} |
| |
| |
| formatted_prompt = apply_chat_template(self.processor, self.config, prompt, num_images=1) |
| |
| |
| output = "" |
| |
| |
| try: |
| for token in stream_generate( |
| self.model, self.processor, formatted_prompt, [image_source], |
| max_tokens=4096, verbose=False |
| ): |
| output += token.text |
| if "</doctag>" in token.text: |
| break |
| except Exception as e: |
| return {"error": f"فشل في تحليل الصورة: {str(e)}"} |
| |
| |
| try: |
| doctags_doc = DocTagsDocument.from_doctags_and_image_pairs([output], [pil_image]) |
| doc = DoclingDocument(name="AnalyzedDocument") |
| doc.load_from_doctags(doctags_doc) |
| |
| |
| return { |
| "doctags": output, |
| "markdown": doc.export_to_markdown(), |
| "document": doc, |
| "image": pil_image |
| } |
| except Exception as e: |
| return {"error": f"فشل في إنشاء مستند Docling: {str(e)}"} |
| |
| except Exception as e: |
| return {"error": f"حدث خطأ غير متوقع: {str(e)}"} |
| |
| def export_to_html(self, doc, output_path="./output.html", show_in_browser=False): |
| """ |
| تصدير المستند إلى HTML |
| |
| المعلمات: |
| doc (DoclingDocument): مستند Docling |
| output_path (str): مسار ملف الإخراج |
| show_in_browser (bool): عرض الملف في المتصفح |
| |
| العوائد: |
| str: مسار ملف HTML المولد |
| """ |
| if not self.is_available(): |
| return None |
| |
| try: |
| from pathlib import Path |
| from docling_core.types.doc import ImageRefMode |
| |
| |
| out_path = Path(output_path) |
| |
| out_path.parent.mkdir(exist_ok=True, parents=True) |
| |
| doc.save_as_html(out_path, image_mode=ImageRefMode.EMBEDDED) |
| |
| |
| if show_in_browser: |
| import webbrowser |
| webbrowser.open(f"file:///{str(out_path.resolve())}") |
| |
| return str(out_path) |
| except Exception as e: |
| print(f"خطأ في تصدير المستند إلى HTML: {str(e)}") |
| return None |
|
|
|
|
| class ClaudeAnalyzer: |
| """ |
| فئة لتحليل المستندات باستخدام Claude.ai API |
| """ |
| def __init__(self): |
| """تهيئة محلل Claude""" |
| self.api_url = "https://api.anthropic.com/v1/messages" |
| |
| def get_api_key(self): |
| """الحصول على مفتاح API من متغيرات البيئة""" |
| api_key = os.environ.get("anthropic") |
| if not api_key: |
| raise ValueError("مفتاح API لـ Claude غير موجود في متغيرات البيئة") |
| return api_key |
| |
| def analyze_document(self, file_path, model_name="claude-3-7-sonnet", prompt=None): |
| """ |
| تحليل مستند باستخدام Claude AI |
| |
| المعلمات: |
| file_path: مسار الملف المراد تحليله |
| model_name: اسم نموذج Claude المراد استخدامه |
| prompt: التوجيه المخصص للتحليل (اختياري) |
| |
| العوائد: |
| dict: نتائج التحليل |
| """ |
| try: |
| |
| api_key = self.get_api_key() |
| |
| |
| if prompt is None: |
| _, ext = os.path.splitext(file_path) |
| ext = ext.lower() |
| |
| if ext == '.pdf': |
| prompt = "قم بتحليل هذه الصورة المستخرجة من مستند PDF واستخراج المعلومات الرئيسية مثل العناوين، الفقرات، الجداول، والنقاط المهمة." |
| elif ext in ('.doc', '.docx'): |
| prompt = "قم بتحليل هذه الصورة المستخرجة من مستند Word واستخراج المعلومات الرئيسية والخلاصة." |
| elif ext in ('.jpg', '.jpeg', '.png', '.gif', '.webp'): |
| prompt = "قم بوصف وتحليل محتوى هذه الصورة بالتفصيل، مع ذكر العناصر المهمة والنصوص والبيانات الموجودة فيها." |
| else: |
| prompt = "قم بتحليل محتوى هذا الملف واستخراج المعلومات المفيدة منه." |
| |
| |
| _, ext = os.path.splitext(file_path) |
| ext = ext.lower() |
| |
| processed_file_path = file_path |
| temp_files = [] |
| |
| |
| if ext not in ('.jpg', '.jpeg', '.png', '.gif', '.webp'): |
| |
| if ext == '.pdf': |
| if not pdf_conversion_available: |
| return {"error": "لا يمكن تحويل ملف PDF إلى صورة. يرجى تثبيت مكتبة pdf2image."} |
| |
| try: |
| |
| images = convert_from_path(file_path, first_page=1, last_page=1) |
| if images: |
| |
| temp_image_path = "/tmp/temp_pdf_image.jpg" |
| images[0].save(temp_image_path, 'JPEG') |
| processed_file_path = temp_image_path |
| temp_files.append(temp_image_path) |
| else: |
| return {"error": "فشل في تحويل ملف PDF إلى صورة"} |
| except Exception as e: |
| return {"error": f"فشل في تحويل ملف PDF إلى صورة: {str(e)}"} |
| else: |
| return {"error": f"نوع الملف {ext} غير مدعوم. Claude API يدعم فقط الصور (JPEG, PNG, GIF, WebP) أو PDF (يتم تحويله تلقائياً)."} |
| |
| |
| try: |
| img = Image.open(processed_file_path) |
| |
| |
| img_width, img_height = img.size |
| if img_width > 1500 or img_height > 1500: |
| |
| img.thumbnail((1500, 1500)) |
| |
| |
| compressed_image_path = "/tmp/compressed_image.jpg" |
| img.save(compressed_image_path, format="JPEG", quality=85) |
| |
| |
| if processed_file_path not in temp_files: |
| temp_files.append(compressed_image_path) |
| |
| processed_file_path = compressed_image_path |
| except Exception as e: |
| logging.warning(f"فشل في ضغط الصورة: {str(e)}. سيتم استخدام الصورة الأصلية.") |
| |
| |
| with open(processed_file_path, 'rb') as f: |
| file_content = f.read() |
| |
| |
| file_size_mb = len(file_content) / (1024 * 1024) |
| if file_size_mb > 20: |
| |
| try: |
| img = Image.open(processed_file_path) |
| |
| |
| compressed_image_path = "/tmp/extra_compressed_image.jpg" |
| img.thumbnail((1000, 1000)) |
| img.save(compressed_image_path, format="JPEG", quality=70) |
| |
| |
| temp_files.append(compressed_image_path) |
| processed_file_path = compressed_image_path |
| |
| |
| with open(processed_file_path, 'rb') as f: |
| file_content = f.read() |
| |
| |
| file_size_mb = len(file_content) / (1024 * 1024) |
| if file_size_mb > 20: |
| |
| for temp_file in temp_files: |
| try: |
| os.unlink(temp_file) |
| except: |
| pass |
| return {"error": f"حجم الملف كبير جدًا ({file_size_mb:.2f} ميجابايت) حتى بعد الضغط. يجب أن يكون أقل من 20 ميجابايت."} |
| except Exception as e: |
| for temp_file in temp_files: |
| try: |
| os.unlink(temp_file) |
| except: |
| pass |
| return {"error": f"الملف كبير جدًا ({file_size_mb:.2f} ميجابايت) ولا يمكن ضغطه. يجب أن يكون أقل من 20 ميجابايت."} |
| |
| |
| file_type = self._get_file_type(processed_file_path) |
| |
| |
| file_base64 = base64.b64encode(file_content).decode('utf-8') |
| |
| |
| headers = { |
| "Content-Type": "application/json", |
| "x-api-key": api_key, |
| "anthropic-version": "2023-06-01" |
| } |
| |
| |
| valid_models = { |
| "claude-3-7-sonnet": "claude-3-7-sonnet-20250219", |
| "claude-3-5-haiku": "claude-3-5-haiku-20240307" |
| } |
| |
| if model_name in valid_models: |
| model_name = valid_models[model_name] |
| |
| |
| logging.debug(f"إرسال طلب إلى Claude API: {model_name}, نوع الملف: {file_type}") |
| |
| |
| payload = { |
| "model": model_name, |
| "max_tokens": 4096, |
| "messages": [ |
| { |
| "role": "user", |
| "content": [ |
| {"type": "text", "text": prompt}, |
| { |
| "type": "image", |
| "source": { |
| "type": "base64", |
| "media_type": file_type, |
| "data": file_base64 |
| } |
| } |
| ] |
| } |
| ] |
| } |
| |
| |
| for attempt in range(3): |
| try: |
| response = requests.post( |
| self.api_url, |
| headers=headers, |
| json=payload, |
| timeout=120 |
| ) |
| |
| |
| if response.status_code == 200: |
| break |
| |
| |
| if response.status_code == 502: |
| wait_time = (attempt + 1) * 5 |
| logging.warning(f"تم استلام خطأ 502. الانتظار {wait_time} ثانية قبل إعادة المحاولة.") |
| time.sleep(wait_time) |
| else: |
| |
| break |
| |
| except requests.exceptions.RequestException as e: |
| logging.warning(f"فشل الطلب في المحاولة {attempt+1}: {str(e)}") |
| if attempt == 2: |
| |
| for temp_file in temp_files: |
| try: |
| os.unlink(temp_file) |
| except: |
| pass |
| return {"error": f"فشل الاتصال بعد عدة محاولات: {str(e)}"} |
| time.sleep((attempt + 1) * 5) |
| |
| |
| for temp_file in temp_files: |
| try: |
| os.unlink(temp_file) |
| except: |
| pass |
| |
| |
| if response.status_code != 200: |
| error_message = f"فشل طلب API: {response.status_code}" |
| try: |
| error_details = response.json() |
| error_message += f"\nتفاصيل: {error_details}" |
| except: |
| error_message += f"\nتفاصيل: {response.text}" |
| |
| return { |
| "error": error_message |
| } |
| |
| |
| result = response.json() |
| |
| return { |
| "success": True, |
| "content": result["content"][0]["text"], |
| "model": result["model"], |
| "usage": result.get("usage", {}) |
| } |
| |
| except Exception as e: |
| |
| for temp_file in temp_files: |
| try: |
| os.unlink(temp_file) |
| except: |
| pass |
| |
| logging.error(f"خطأ أثناء تحليل المستند: {str(e)}") |
| import traceback |
| stack_trace = traceback.format_exc() |
| return {"error": f"فشل في تحليل المستند: {str(e)}\n{stack_trace}"} |
| |
| def _get_file_type(self, file_path): |
| """تحديد نوع الملف من امتداده""" |
| _, ext = os.path.splitext(file_path) |
| ext = ext.lower() |
| |
| |
| if ext in ('.jpg', '.jpeg'): |
| return "image/jpeg" |
| elif ext == '.png': |
| return "image/png" |
| elif ext == '.gif': |
| return "image/gif" |
| elif ext == '.webp': |
| return "image/webp" |
| else: |
| |
| |
| return "image/jpeg" |
| |
| def get_available_models(self): |
| """ |
| الحصول على قائمة بالنماذج المتاحة |
| |
| العوائد: |
| dict: قائمة بالنماذج مع وصفها |
| """ |
| return { |
| "claude-3-7-sonnet": "Claude 3.7 Sonnet - نموذج ذكي للمهام المتقدمة", |
| "claude-3-5-haiku": "Claude 3.5 Haiku - أسرع نموذج للمهام اليومية" |
| } |
| |
| def get_model_full_name(self, short_name): |
| """ |
| تحويل الاسم المختصر للنموذج إلى الاسم الكامل |
| |
| المعلمات: |
| short_name: الاسم المختصر للنموذج |
| |
| العوائد: |
| str: الاسم الكامل للنموذج |
| """ |
| valid_models = { |
| "claude-3-7-sonnet": "claude-3-7-sonnet-20250219", |
| "claude-3-5-haiku": "claude-3-5-haiku-20240307" |
| } |
| |
| return valid_models.get(short_name, short_name) |
|
|
|
|
| class DocumentAnalysisApp: |
| def __init__(self): |
| |
| self.text_extractor = TextExtractor() |
| self.item_extractor = ItemExtractor() |
| self.document_parser = DocumentParser() |
| |
| |
| self.docling_analyzer = DoclingAnalyzer() |
| |
| |
| self.claude_analyzer = ClaudeAnalyzer() |
|
|
| def render(self): |
| """العرض الرئيسي للتطبيق""" |
| st.title("تحليل المستندات") |
| st.write("اختر ملفًا لتحليله واستخرج البيانات المطلوبة.") |
|
|
| |
| tabs = st.tabs(["تحليل عام", "تحليل Docling", "تحليل Claude AI"]) |
| |
| with tabs[0]: |
| self._render_general_analysis() |
| |
| with tabs[1]: |
| self._render_docling_analysis() |
| |
| with tabs[2]: |
| self._render_claude_analysis() |
| |
| def _render_general_analysis(self): |
| """عرض واجهة التحليل العام""" |
| uploaded_file = st.file_uploader("ارفع ملف PDF أو DOCX", type=["pdf", "docx"], key="general_uploader") |
| |
| if uploaded_file: |
| with st.spinner("جاري تحليل المستند..."): |
| file_path = f"/tmp/{uploaded_file.name}" |
| with open(file_path, "wb") as f: |
| f.write(uploaded_file.read()) |
|
|
| |
| _, ext = os.path.splitext(file_path) |
| ext = ext.lower() |
|
|
| |
| if ext == '.pdf': |
| extracted_text = self.text_extractor.extract_from_pdf(file_path) |
| elif ext in ('.doc', '.docx'): |
| extracted_text = self.text_extractor.extract_from_docx(file_path) |
| else: |
| extracted_text = "نوع ملف غير مدعوم للنص" |
|
|
| |
| st.subheader("النص المستخرج:") |
| st.text_area("النص", extracted_text, height=300) |
|
|
| |
| extracted_items = self.item_extractor.extract(file_path) |
| if extracted_items: |
| st.subheader("البنود المستخرجة:") |
| st.dataframe(extracted_items) |
|
|
| |
| parsed_data = self.document_parser.parse(file_path) |
| st.subheader("تحليل المستند:") |
| st.json(parsed_data) |
| |
| def _render_docling_analysis(self): |
| """عرض واجهة تحليل Docling""" |
| import streamlit as st |
| from tempfile import NamedTemporaryFile |
| |
| if not self.docling_analyzer.is_available(): |
| st.warning("مكتبات Docling و MLX VLM غير متوفرة. يرجى تثبيت الحزم المطلوبة.") |
| st.code(""" |
| # يرجى تثبيت الحزم التالية: |
| pip install docling-core mlx-vlm pillow>=10.3.0 transformers>=4.49.0 tqdm>=4.66.2 |
| """) |
| return |
| |
| st.subheader("تحليل الصور والمستندات باستخدام Docling") |
| |
| |
| source_option = st.radio("اختر مصدر الصورة:", ["رفع صورة", "رابط صورة"]) |
| |
| image_path = None |
| image_url = None |
| image_data = None |
| |
| if source_option == "رفع صورة": |
| uploaded_image = st.file_uploader("ارفع صورة", type=["jpg", "jpeg", "png"], key="docling_uploader") |
| if uploaded_image: |
| |
| image_data = uploaded_image.read() |
| |
| |
| st.image(image_data, caption="الصورة المرفوعة", width=400) |
| |
| |
| with NamedTemporaryFile(delete=False, suffix=f".{uploaded_image.name.split('.')[-1]}") as temp_file: |
| temp_file.write(image_data) |
| image_path = temp_file.name |
| else: |
| image_url = st.text_input("أدخل رابط الصورة:") |
| if image_url: |
| try: |
| |
| st.image(image_url, caption="الصورة من الرابط", width=400) |
| except Exception as e: |
| st.error(f"خطأ في تحميل الصورة: {str(e)}") |
| |
| |
| prompt = st.text_input("توجيه للنموذج:", value="Convert this page to docling.") |
| |
| |
| if st.button("تحليل الصورة"): |
| if image_path or image_url: |
| with st.spinner("جاري تحليل الصورة..."): |
| |
| results = self.docling_analyzer.analyze_image( |
| image_path=image_path, |
| image_url=image_url, |
| image_bytes=None, |
| prompt=prompt |
| ) |
| |
| if "error" in results: |
| st.error(results["error"]) |
| else: |
| |
| with st.expander("علامات DocTags", expanded=True): |
| st.code(results["doctags"], language="xml") |
| |
| with st.expander("Markdown", expanded=True): |
| st.code(results["markdown"], language="markdown") |
| |
| |
| if st.button("تصدير إلى HTML"): |
| html_path = self.docling_analyzer.export_to_html( |
| results["document"], |
| show_in_browser=True |
| ) |
| if html_path: |
| st.success(f"تم تصدير المستند إلى: {html_path}") |
| else: |
| st.error("فشل تصدير المستند إلى HTML") |
| |
| |
| if image_path and os.path.exists(image_path) and image_data: |
| try: |
| os.unlink(image_path) |
| except: |
| pass |
| else: |
| st.warning("يرجى اختيار صورة للتحليل أولاً.") |
| |
| def _render_claude_analysis(self): |
| """عرض واجهة تحليل Claude AI مع توسعة البيانات المعروضة""" |
| import time |
| |
| st.subheader("تحليل المستندات باستخدام Claude AI") |
| |
| col1, col2 = st.columns([2, 1]) |
| |
| with col1: |
| |
| claude_models = { |
| "claude-3-7-sonnet": "Claude 3.7 Sonnet - نموذج ذكي للمهام المتقدمة", |
| "claude-3-5-haiku": "Claude 3.5 Haiku - أسرع نموذج للمهام اليومية" |
| } |
| |
| selected_model = st.radio( |
| "اختر نموذج Claude", |
| options=list(claude_models.keys()), |
| format_func=lambda x: claude_models[x], |
| horizontal=True |
| ) |
| |
| with col2: |
| |
| if selected_model == "claude-3-7-sonnet": |
| st.info("نموذج Claude 3.7 Sonnet هو أحدث نموذج ذكي يقدم تحليلاً متعمقاً للمستندات مع دقة عالية") |
| else: |
| st.info("نموذج Claude 3.5 Haiku أسرع في التحليل ومناسب للمهام البسيطة والاستخدام اليومي") |
| |
| |
| st.subheader("تخصيص التحليل") |
| |
| prompt_templates = { |
| "تحليل عام": "قم بتحليل هذا المستند واستخراج جميع المعلومات المهمة.", |
| "استخراج البيانات الأساسية": "استخرج كافة البيانات الأساسية من هذا المستند بما في ذلك الأسماء والتواريخ والأرقام والمبالغ المالية.", |
| "تلخيص المستند": "قم بتلخيص هذا المستند بشكل مفصل مع التركيز على النقاط الرئيسية.", |
| "تحليل العقود": "حلل هذا العقد واستخرج الأطراف والالتزامات والشروط والتواريخ المهمة.", |
| "تحليل فواتير": "استخرج كافة المعلومات من هذه الفاتورة بما في ذلك المورد والعميل وتفاصيل المنتجات والأسعار والمبالغ الإجمالية." |
| } |
| |
| prompt_type = st.selectbox( |
| "اختر نوع التوجيه", |
| options=list(prompt_templates.keys()), |
| index=0 |
| ) |
| |
| default_prompt = prompt_templates[prompt_type] |
| |
| custom_prompt = st.text_area( |
| "تخصيص التوجيه للتحليل", |
| value=default_prompt, |
| height=100 |
| ) |
| |
| |
| with st.expander("خيارات متقدمة"): |
| extraction_format = st.selectbox( |
| "تنسيق استخراج البيانات", |
| ["عام", "جداول", "قائمة", "هيكل منظم"], |
| index=0 |
| ) |
| |
| detail_level = st.slider( |
| "مستوى التفاصيل", |
| min_value=1, |
| max_value=5, |
| value=3, |
| help="1: ملخص موجز، 5: تحليل تفصيلي كامل" |
| ) |
| |
| |
| if extraction_format != "عام" or detail_level != 3: |
| custom_prompt += f"\n\nاستخدم تنسيق {extraction_format} مع مستوى تفاصيل {detail_level}/5." |
| |
| |
| uploaded_file = st.file_uploader( |
| "ارفع ملفًا للتحليل", |
| type=["pdf", "jpg", "jpeg", "png"], |
| key="claude_uploader", |
| help="يدعم ملفات PDF والصور. سيتم تحويل PDF إلى صور لمعالجتها." |
| ) |
| |
| |
| api_available = True |
| try: |
| self.claude_analyzer.get_api_key() |
| except ValueError: |
| api_available = False |
| st.warning("مفتاح API لـ Claude غير متوفر. يرجى التأكد من تعيين متغير البيئة 'anthropic'.") |
| |
| |
| analyze_col1, analyze_col2 = st.columns([1, 3]) |
| |
| with analyze_col1: |
| analyze_button = st.button( |
| "تحليل المستند", |
| key="analyze_claude_btn", |
| use_container_width=True, |
| disabled=not (uploaded_file and api_available) |
| ) |
| |
| with analyze_col2: |
| if not uploaded_file: |
| st.info("يرجى رفع ملف للتحليل") |
| |
| |
| if uploaded_file and api_available and analyze_button: |
| |
| progress_bar = st.progress(0, text="جاري تجهيز الملف...") |
| |
| with st.spinner(f"جاري التحليل باستخدام {claude_models[selected_model].split('-')[0]}..."): |
| |
| temp_path = f"/tmp/{uploaded_file.name}" |
| with open(temp_path, "wb") as f: |
| f.write(uploaded_file.getbuffer()) |
| |
| |
| progress_bar.progress(25, text="جاري معالجة الملف...") |
| |
| try: |
| |
| progress_bar.progress(40, text="جاري إرسال الطلب إلى Claude AI...") |
| |
| results = self.claude_analyzer.analyze_document( |
| temp_path, |
| model_name=selected_model, |
| prompt=custom_prompt |
| ) |
| |
| progress_bar.progress(90, text="جاري معالجة النتائج...") |
| |
| if "error" in results: |
| st.error(results["error"]) |
| else: |
| progress_bar.progress(100, text="اكتمل التحليل!") |
| |
| |
| st.success(f"تم التحليل بنجاح باستخدام {results.get('model', selected_model)}!") |
| |
| |
| result_tabs = st.tabs(["التحليل الكامل", "بيانات مستخرجة", "معلومات إضافية"]) |
| |
| with result_tabs[0]: |
| |
| st.markdown("## نتائج التحليل") |
| st.markdown(results["content"]) |
| |
| with result_tabs[1]: |
| |
| st.markdown("## البيانات المستخرجة") |
| |
| |
| content_parts = results["content"].split("\n\n") |
| |
| |
| headings = [] |
| key_values = {} |
| |
| for part in content_parts: |
| |
| if part.startswith("#") or part.startswith("##") or part.startswith("###"): |
| headings.append(part.strip()) |
| continue |
| |
| |
| if ":" in part and len(part.split(":")) == 2: |
| key, value = part.split(":") |
| key_values[key.strip()] = value.strip() |
| |
| |
| if headings: |
| st.markdown("### العناوين الرئيسية") |
| for heading in headings[:5]: |
| st.markdown(f"- {heading}") |
| |
| if len(headings) > 5: |
| with st.expander(f"عرض {len(headings) - 5} عناوين إضافية"): |
| for heading in headings[5:]: |
| st.markdown(f"- {heading}") |
| |
| |
| if key_values: |
| st.markdown("### بيانات هامة") |
| |
| |
| import pandas as pd |
| df = pd.DataFrame([key_values.values()], columns=key_values.keys()) |
| st.dataframe(df.T) |
| |
| |
| if "| ------ |" in results["content"] or "\n|" in results["content"]: |
| st.markdown("### جداول مستخرجة") |
| |
| table_parts = [] |
| in_table = False |
| current_table = [] |
| |
| for line in results["content"].split("\n"): |
| if line.startswith("|") and "-|-" in line.replace(" ", ""): |
| in_table = True |
| current_table.append(line) |
| elif in_table and line.startswith("|"): |
| current_table.append(line) |
| elif in_table and not line.startswith("|") and line.strip(): |
| in_table = False |
| table_parts.append("\n".join(current_table)) |
| current_table = [] |
| |
| |
| if current_table: |
| table_parts.append("\n".join(current_table)) |
| |
| |
| for i, table in enumerate(table_parts): |
| st.markdown(f"#### جدول {i+1}") |
| st.markdown(table) |
| |
| |
| if not headings and not key_values and not ("| ------ |" in results["content"] or "\n|" in results["content"]): |
| st.info("لم يتم العثور على بيانات منظمة في النتائج. يمكنك تعديل التوجيه لطلب تنسيق أكثر هيكلية.") |
| |
| with result_tabs[2]: |
| |
| st.markdown("## معلومات عن التحليل") |
| |
| |
| col1, col2 = st.columns(2) |
| |
| with col1: |
| st.markdown("### معلومات النموذج") |
| st.markdown(f"**النموذج المستخدم**: {results.get('model', selected_model)}") |
| st.markdown(f"**تاريخ التحليل**: {time.strftime('%Y-%m-%d %H:%M:%S')}") |
| |
| with col2: |
| st.markdown("### إحصائيات الاستخدام") |
| |
| if "usage" in results: |
| usage = results["usage"] |
| st.markdown(f"**توكنز المدخلات**: {usage.get('input_tokens', 'غير متوفر')}") |
| st.markdown(f"**توكنز الإخراج**: {usage.get('output_tokens', 'غير متوفر')}") |
| st.markdown(f"**إجمالي التوكنز**: {usage.get('input_tokens', 0) + usage.get('output_tokens', 0)}") |
| else: |
| st.info("معلومات الاستخدام غير متوفرة") |
| |
| |
| st.markdown("### تصدير النتائج") |
| |
| export_col1, export_col2 = st.columns(2) |
| |
| with export_col1: |
| |
| st.download_button( |
| label="تحميل النتائج كملف نصي", |
| data=results["content"], |
| file_name=f"claude_analysis_{uploaded_file.name.split('.')[0]}.txt", |
| mime="text/plain" |
| ) |
| |
| with export_col2: |
| |
| st.download_button( |
| label="تحميل النتائج كملف Markdown", |
| data=results["content"], |
| file_name=f"claude_analysis_{uploaded_file.name.split('.')[0]}.md", |
| mime="text/markdown" |
| ) |
| finally: |
| |
| try: |
| os.unlink(temp_path) |
| except: |
| pass |
|
|
| def analyze_document(self, file_path): |
| """ |
| تحليل مستند وإرجاع نتائج التحليل |
| |
| المعلمات: |
| file_path (str): مسار المستند المراد تحليله |
| |
| العوائد: |
| dict: نتائج تحليل المستند |
| """ |
| |
| _, ext = os.path.splitext(file_path) |
| ext = ext.lower() |
| |
| |
| if ext == '.pdf': |
| text = self.text_extractor.extract_from_pdf(file_path) |
| elif ext in ('.doc', '.docx'): |
| text = self.text_extractor.extract_from_docx(file_path) |
| elif ext in ('.jpg', '.jpeg', '.png'): |
| |
| if self.docling_analyzer.is_available(): |
| docling_results = self.docling_analyzer.analyze_image(image_path=file_path) |
| if "error" not in docling_results: |
| return { |
| "نص": docling_results["markdown"], |
| "doctags": docling_results["doctags"], |
| "معلومات": { |
| "نوع المستند": "صورة", |
| "تحليل": "تم تحليله باستخدام Docling" |
| } |
| } |
| |
| |
| text = self.text_extractor.extract_from_image(file_path) |
| else: |
| raise ValueError(f"نوع المستند غير مدعوم: {ext}") |
| |
| |
| document = self.document_parser.parse_document(file_path) |
| |
| |
| tables = self.item_extractor.extract_tables(document) |
| |
| |
| return { |
| "نص": text, |
| "جداول": tables, |
| "معلومات": document |
| } |
| |
| def analyze_with_claude(self, file_path, model_name="claude-3-7-sonnet", prompt=None): |
| """ |
| تحليل مستند باستخدام Claude AI |
| |
| المعلمات: |
| file_path (str): مسار المستند المراد تحليله |
| model_name (str): اسم نموذج Claude المراد استخدامه |
| prompt (str): التوجيه المخصص للتحليل (اختياري) |
| |
| العوائد: |
| dict: نتائج التحليل |
| """ |
| |
| try: |
| |
| self.claude_analyzer.get_api_key() |
| |
| |
| return self.claude_analyzer.analyze_document( |
| file_path, |
| model_name=model_name, |
| prompt=prompt |
| ) |
| except Exception as e: |
| logging.error(f"خطأ في تحليل المستند باستخدام Claude: {str(e)}") |
| return {"error": f"فشل في تحليل المستند باستخدام Claude: {str(e)}"} |
|
|
|
|
| |
| if __name__ == "__main__": |
| app = DocumentAnalysisApp() |
| app.render() |