|
|
|
""" |
|
وحدة تطبيق تحليل المستندات |
|
|
|
هذا الملف يحتوي على الفئة الرئيسية لتطبيق تحليل المستندات. |
|
""" |
|
|
|
|
|
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() |