Really-amin commited on
Commit
8be7fed
·
verified ·
1 Parent(s): 938561f

Upload persian_legal_scraper.py

Browse files
Files changed (1) hide show
  1. app/persian_legal_scraper.py +1720 -0
app/persian_legal_scraper.py ADDED
@@ -0,0 +1,1720 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py - سیستم پیشرفته استخراج و تحلیل اسناد حقوقی فارسی
2
+ import os
3
+ import gc
4
+ import sys
5
+ import time
6
+ import json
7
+ import logging
8
+ import resource
9
+ import requests
10
+ import threading
11
+ import re
12
+ import random
13
+ from pathlib import Path
14
+ from datetime import datetime
15
+ from typing import List, Dict, Any, Optional, Tuple, Union
16
+ from dataclasses import dataclass
17
+ from concurrent.futures import ThreadPoolExecutor, as_completed
18
+ from urllib.parse import urljoin, urlparse
19
+ import hashlib
20
+
21
+ import gradio as gr
22
+ import pandas as pd
23
+ import torch
24
+ from bs4 import BeautifulSoup
25
+ from transformers import (
26
+ AutoTokenizer,
27
+ AutoModelForSequenceClassification,
28
+ pipeline,
29
+ logging as transformers_logging
30
+ )
31
+ import warnings
32
+
33
+ # تنظیمات اولیه
34
+ warnings.filterwarnings('ignore')
35
+ transformers_logging.set_verbosity_error()
36
+
37
+ # محدودیت حافظه برای HF Spaces
38
+ try:
39
+ resource.setrlimit(resource.RLIMIT_AS, (2*1024*1024*1024, 2*1024*1024*1024))
40
+ except:
41
+ pass
42
+
43
+ # تنظیمات محیط
44
+ os.environ['TRANSFORMERS_CACHE'] = '/tmp/hf_cache'
45
+ os.environ['HF_HOME'] = '/tmp/hf_cache'
46
+ os.environ['TORCH_HOME'] = '/tmp/torch_cache'
47
+ os.environ['TOKENIZERS_PARALLELISM'] = 'false'
48
+
49
+ # تنظیم لاگ
50
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
51
+ logger = logging.getLogger(__name__)
52
+
53
+ # SVG Icons
54
+ SVG_ICONS = {
55
+ 'search': '🔍',
56
+ 'document': '📄',
57
+ 'analyze': '🤖',
58
+ 'export': '📊',
59
+ 'settings': '⚙️',
60
+ 'preview': '👁️',
61
+ 'link': '🔗',
62
+ 'success': '✅',
63
+ 'error': '❌',
64
+ 'warning': '⚠️'
65
+ }
66
+
67
+ # منابع حقوقی معتبر ایران
68
+ LEGAL_SOURCES_CONFIG = {
69
+ "مجلس شورای اسلامی": {
70
+ "base_url": "https://rc.majlis.ir",
71
+ "patterns": ["/fa/law/", "/fa/report/", "/fa/news/"],
72
+ "selectors": [".main-content", ".article-body", ".content", "article"],
73
+ "delay_range": (2, 5),
74
+ "max_depth": 2
75
+ },
76
+ "پورتال ملی قوانین": {
77
+ "base_url": "https://www.dotic.ir",
78
+ "patterns": ["/portal/", "/law/", "/regulation/"],
79
+ "selectors": [".content-area", ".law-content", ".main-text"],
80
+ "delay_range": (1, 4),
81
+ "max_depth": 2
82
+ },
83
+ "قوه قضاییه": {
84
+ "base_url": "https://www.judiciary.ir",
85
+ "patterns": ["/fa/news/", "/fa/verdict/", "/fa/law/"],
86
+ "selectors": [".news-content", ".verdict-text", ".main-content"],
87
+ "delay_range": (3, 6),
88
+ "max_depth": 2
89
+ },
90
+ "وزارت دادگستری": {
91
+ "base_url": "https://www.moj.ir",
92
+ "patterns": ["/fa/news/", "/fa/law/", "/fa/regulation/"],
93
+ "selectors": [".news-body", ".law-text", ".content"],
94
+ "delay_range": (2, 4),
95
+ "max_depth": 2
96
+ },
97
+ "دیوان عدالت اداری": {
98
+ "base_url": "https://www.adcourt.ir",
99
+ "patterns": ["/fa/verdict/", "/fa/news/"],
100
+ "selectors": [".verdict-content", ".news-text"],
101
+ "delay_range": (1, 3),
102
+ "max_depth": 2
103
+ }
104
+ }
105
+
106
+ # واژگان حقوقی فارسی پیشرفته
107
+ PERSIAN_LEGAL_DICTIONARY = {
108
+ "قوانین_اساسی": [
109
+ "قانون اساسی", "اصول قانون اساسی", "مجلس شورای اسلامی", "شورای نگهبان",
110
+ "رهبری", "جمهوری اسلامی", "حاکمیت ملی", "ولایت فقیه"
111
+ ],
112
+ "قوانین_عادی": [
113
+ "ماده", "تبصره", "اصل", "فصل", "باب", "قسمت", "بخش", "کتاب",
114
+ "قانون مدنی", "قانون جزا", "قانون آیین دادرسی", "قانون تجارت"
115
+ ],
116
+ "مقررات_اجرایی": [
117
+ "آیین‌نامه", "مقرره", "دستورالعمل", "شیوه‌نامه", "بند", "جزء", "فقره",
118
+ "ضابطه", "رهنمود", "دستور", "اعلامیه", "ابلاغیه"
119
+ ],
120
+ "اصطلاحات_حقوقی": [
121
+ "شخص حقیقی", "شخص حقوقی", "حق", "تکلیف", "مسئولیت", "جرم", "مجازات",
122
+ "دعوا", "طرف دعوا", "خواهان", "خوانده", "شاکی", "متهم", "مجنی‌علیه"
123
+ ],
124
+ "نهادهای_قضایی": [
125
+ "دادگاه", "قاضی", "دادرس", "مدعی‌العموم", "وکیل", "کارشناس", "مترجم",
126
+ "رای", "حکم", "قرار", "اجرائیه", "کیفرخواست", "لایحه دفاعیه"
127
+ ],
128
+ "اصطلاحات_اداری": [
129
+ "وزارت", "اداره", "سازمان", "مدیر", "مقام", "مسئول", "کارمند", "کارگزار",
130
+ "بخشنامه", "تصویب‌نامه", "مصوبه", "تصمیم", "نظریه", "استعلام"
131
+ ],
132
+ "مفاهیم_مالی": [
133
+ "مالیات", "عوارض", "پرداخت", "وجه", "ریال", "درهم", "خسارت", "دیه",
134
+ "تأمین", "ضمانت", "وثیقه", "سپرده", "جریمه", "جزا�� نقدی"
135
+ ]
136
+ }
137
+
138
+ @dataclass
139
+ class ProcessingProgress:
140
+ current_step: str = ""
141
+ progress: float = 0.0
142
+ total_documents: int = 0
143
+ processed_documents: int = 0
144
+ status: str = "آماده"
145
+ error: Optional[str] = None
146
+
147
+ class MemoryManager:
148
+ """مدیریت هوشمند حافظه برای HF Spaces"""
149
+
150
+ @staticmethod
151
+ def get_memory_usage() -> float:
152
+ try:
153
+ with open('/proc/self/status') as f:
154
+ for line in f:
155
+ if line.startswith('VmRSS:'):
156
+ return float(line.split()[1]) / 1024
157
+ return 0.0
158
+ except:
159
+ return 0.0
160
+
161
+ @staticmethod
162
+ def check_memory_available(required_mb: float) -> bool:
163
+ try:
164
+ current_usage = MemoryManager.get_memory_usage()
165
+ return current_usage + required_mb < 1800
166
+ except:
167
+ return True
168
+
169
+ @staticmethod
170
+ def cleanup_memory():
171
+ gc.collect()
172
+ if torch.cuda.is_available():
173
+ torch.cuda.empty_cache()
174
+
175
+ class SmartTextProcessor:
176
+ """پردازشگر هوشمند متن با قابلیت‌های پیشرفته"""
177
+
178
+ def __init__(self):
179
+ self.sentence_endings = ['۔', '.', '!', '؟', '?', ';', '؛']
180
+ self.paragraph_indicators = ['ماده', 'تبصره', 'بند', 'الف', 'ب', 'ج', 'د', 'ه', 'و']
181
+
182
+ # الگوهای شناسایی ارجاعات حقوقی
183
+ self.citation_patterns = [
184
+ r'ماده\s*(\d+)',
185
+ r'تبصره\s*(\d+)',
186
+ r'بند\s*([الف-ی]|\d+)',
187
+ r'فصل\s*(\d+)',
188
+ r'باب\s*(\d+)',
189
+ r'قسمت\s*(\d+)',
190
+ r'اصل\s*(\d+)'
191
+ ]
192
+
193
+ # الگوهای پاکسازی متن
194
+ self.cleanup_patterns = [
195
+ (r'\s+', ' '),
196
+ (r'([۰-۹])\s+([۰-۹])', r'\1\2'),
197
+ (r'([a-zA-Z])\s+([a-zA-Z])', r'\1\2'),
198
+ (r'([ا-ی])\s+(ها|های|ان|ات|ین)', r'\1\2'),
199
+ (r'(می|نمی|خواهد)\s+(شود|گردد|باشد)', r'\1‌\2'),
200
+ ]
201
+
202
+ def normalize_persian_text(self, text: str) -> str:
203
+ """نرمال‌سازی پیشرفته متن فارسی"""
204
+ if not text:
205
+ return ""
206
+
207
+ # نرمال‌سازی کاراکترهای فارسی
208
+ persian_normalization = {
209
+ 'ي': 'ی', 'ك': 'ک', 'ة': 'ه', 'ؤ': 'و', 'إ': 'ا', 'أ': 'ا',
210
+ 'ء': '', 'ئ': 'ی', '٠': '۰', '١': '۱', '٢': '۲', '٣': '۳', '٤': '۴',
211
+ '٥': '۵', '٦': '۶', '٧': '۷', '٨': '۸', '٩': '۹'
212
+ }
213
+
214
+ for old, new in persian_normalization.items():
215
+ text = text.replace(old, new)
216
+
217
+ # اعمال الگوهای پاکسازی
218
+ for pattern, replacement in self.cleanup_patterns:
219
+ text = re.sub(pattern, replacement, text)
220
+
221
+ # حذف کاراکترهای غیرضروری
222
+ text = re.sub(r'[^\u0600-\u06FF\u200C\u200D\s\w\d.,;:!؟()«»\-]', '', text)
223
+
224
+ return text.strip()
225
+
226
+ def detect_long_sentences(self, text: str) -> List[Dict[str, Any]]:
227
+ """تشخیص جملات طولانی و پیچیده"""
228
+ sentences = self._split_into_sentences(text)
229
+ long_sentences = []
230
+
231
+ for i, sentence in enumerate(sentences):
232
+ sentence_info = {
233
+ 'index': i,
234
+ 'text': sentence,
235
+ 'word_count': len(sentence.split()),
236
+ 'is_long': False,
237
+ 'suggestions': []
238
+ }
239
+
240
+ # بررسی طول جمله
241
+ if sentence_info['word_count'] > 30:
242
+ sentence_info['is_long'] = True
243
+ sentence_info['suggestions'].append('جمله بسیار طولانی - تقسیم توصیه می‌شود')
244
+
245
+ # بررسی پیچیدگی
246
+ complexity_score = self._calculate_complexity(sentence)
247
+ if complexity_score > 5:
248
+ sentence_info['suggestions'].append('جمله پیچیده - ساده‌سازی توصیه می‌شود')
249
+
250
+ if sentence_info['is_long'] or sentence_info['suggestions']:
251
+ long_sentences.append(sentence_info)
252
+
253
+ return long_sentences
254
+
255
+ def _split_into_sentences(self, text: str) -> List[str]:
256
+ """تقسیم متن به جملات"""
257
+ boundaries = self.detect_sentence_boundaries(text)
258
+ sentences = []
259
+
260
+ start = 0
261
+ for boundary in boundaries:
262
+ sentence = text[start:boundary].strip()
263
+ if sentence and len(sentence) > 5:
264
+ sentences.append(sentence)
265
+ start = boundary
266
+
267
+ # جمله آخر
268
+ if start < len(text):
269
+ last_sentence = text[start:].strip()
270
+ if last_sentence and len(last_sentence) > 5:
271
+ sentences.append(last_sentence)
272
+
273
+ return sentences
274
+
275
+ def _calculate_complexity(self, sentence: str) -> float:
276
+ """محاسبه پیچیدگی جمله"""
277
+ complexity = 0
278
+
279
+ # تعداد کلمات ربط
280
+ conjunctions = ['که', 'اگر', 'چون', 'زیرا', 'ولی', 'اما', 'درحالیکه', 'درصورتیکه']
281
+ complexity += sum(sentence.count(conj) for conj in conjunctions) * 0.5
282
+
283
+ # تعداد ویرگول
284
+ complexity += sentence.count('،') * 0.3
285
+
286
+ # تعداد کلمات
287
+ complexity += len(sentence.split()) * 0.02
288
+
289
+ # جملات تودرتو
290
+ if sentence.count('(') > 0:
291
+ complexity += sentence.count('(') * 0.5
292
+
293
+ return complexity
294
+
295
+ def detect_sentence_boundaries(self, text: str) -> List[int]:
296
+ """تشخیص هوشمند مرزهای جمله در متون حقوقی فارسی"""
297
+ boundaries = []
298
+
299
+ for i, char in enumerate(text):
300
+ if char in self.sentence_endings:
301
+ is_real_ending = True
302
+
303
+ # بررسی برای اعداد و اختصارات
304
+ if i > 0 and text[i-1].isdigit() and char == '.':
305
+ is_real_ending = False
306
+
307
+ # بررسی برای اختصارات رایج
308
+ if i > 2:
309
+ prev_text = text[max(0, i-10):i].strip()
310
+ if any(abbr in prev_text for abbr in ['ماده', 'بند', 'ج.ا.ا', 'ق.م', 'ق.ج']):
311
+ if char == '.' and i < len(text) - 1 and not text[i+1].isspace():
312
+ is_real_ending = False
313
+
314
+ if is_real_ending:
315
+ if i < len(text) - 1:
316
+ next_char = text[i + 1]
317
+ if next_char.isspace() or next_char in '«"\'':
318
+ boundaries.append(i + 1)
319
+ else:
320
+ boundaries.append(i + 1)
321
+
322
+ return boundaries
323
+
324
+ def reconstruct_legal_text(self, content_fragments: List[str]) -> str:
325
+ """بازسازی هوشمند متن حقوقی از قطعات پراکنده"""
326
+ if not content_fragments:
327
+ return ""
328
+
329
+ # مرحله 1: نرمال‌سازی تمام قطعات
330
+ normalized_fragments = []
331
+ for fragment in content_fragments:
332
+ normalized = self.normalize_persian_text(fragment)
333
+ if normalized and len(normalized.strip()) > 10:
334
+ normalized_fragments.append(normalized)
335
+
336
+ if not normalized_fragments:
337
+ return ""
338
+
339
+ # مرحله 2: ادغام هوشمند قطعات
340
+ combined_text = self._smart_join_fragments(normalized_fragments)
341
+
342
+ # مرحله 3: اعمال قالب‌بندی حقوقی
343
+ formatted = self._apply_legal_formatting(combined_text)
344
+
345
+ return formatted
346
+
347
+ def _smart_join_fragments(self, fragments: List[str]) -> str:
348
+ """ادغام هوشمند قطعات با در نظر گیری زمینه"""
349
+ if len(fragments) == 1:
350
+ return fragments[0]
351
+
352
+ result = [fragments[0]]
353
+
354
+ for i in range(1, len(fragments)):
355
+ current_fragment = fragments[i]
356
+ prev_fragment = result[-1]
357
+
358
+ # بررسی ادامه جمله
359
+ if self._should_continue_sentence(prev_fragment, current_fragment):
360
+ result[-1] += ' ' + current_fragment
361
+ # بررسی ادغام بدون فاصله (نیم‌فاصله)
362
+ elif self._should_join_without_space(prev_fragment, current_fragment):
363
+ result[-1] += current_fragment
364
+ # شروع پاراگراف جدید
365
+ elif self._is_new_paragraph(current_fragment):
366
+ result.append('\n\n' + current_fragment)
367
+ else:
368
+ result.append(' ' + current_fragment)
369
+
370
+ return ''.join(result)
371
+
372
+ def _should_continue_sentence(self, prev: str, current: str) -> bool:
373
+ """تشخیص ادامه جمله"""
374
+ # کلمات ادامه‌دهنده
375
+ continuation_words = ['که', 'تا', 'اگر', 'چون', 'زیرا', 'ولی', 'اما', 'و', 'یا']
376
+
377
+ # اگر جمله قبلی ناتمام باشد
378
+ if not any(prev.endswith(end) for end in self.sentence_endings):
379
+ return True
380
+
381
+ # اگر جمله فعلی با کلمه ادامه شروع شود
382
+ if any(current.strip().startswith(word) for word in continuation_words):
383
+ return True
384
+
385
+ return False
386
+
387
+ def _should_join_without_space(self, prev: str, current: str) -> bool:
388
+ """تشخیص ادغام بدون فاصله"""
389
+ # پسوندها و پیشوندها
390
+ suffixes = ['��ا', 'های', 'ان', 'ات', 'ین', 'تان', 'شان', 'تون', 'شون']
391
+ prefixes = ['می‌', 'نمی‌', 'برمی‌', 'درمی‌']
392
+
393
+ current_stripped = current.strip()
394
+
395
+ # بررسی پسوندها
396
+ if any(current_stripped.startswith(suffix) for suffix in suffixes):
397
+ return True
398
+
399
+ # بررسی ادامه فعل
400
+ if prev.endswith(('می', 'نمی', 'خواهد', 'است')):
401
+ verb_continuations = ['شود', 'گردد', 'باشد', 'کرد', 'کند']
402
+ if any(current_stripped.startswith(cont) for cont in verb_continuations):
403
+ return True
404
+
405
+ return False
406
+
407
+ def _is_new_paragraph(self, text: str) -> bool:
408
+ """تشخیص شروع پاراگراف جدید"""
409
+ text_stripped = text.strip()
410
+
411
+ # شاخص‌های پاراگراف در متون حقوقی
412
+ paragraph_starters = [
413
+ 'ماده', 'تبصره', 'بند', 'فصل', 'باب', 'قسمت', 'کتاب',
414
+ 'الف)', 'ب)', 'ج)', 'د)', 'ه)', 'و)', 'ز)', 'ح)', 'ط)',
415
+ '۱-', '۲-', '۳-', '۴-', '۵-', '۶-', '۷-', '۸-', '۹-', '۱۰-'
416
+ ]
417
+
418
+ return any(text_stripped.startswith(starter) for starter in paragraph_starters)
419
+
420
+ def _apply_legal_formatting(self, text: str) -> str:
421
+ """اعمال قالب‌بندی مخصوص اسناد حقوقی"""
422
+ lines = text.split('\n')
423
+ formatted_lines = []
424
+
425
+ for line in lines:
426
+ line = line.strip()
427
+ if not line:
428
+ continue
429
+
430
+ # قالب‌بندی مواد و تبصره‌ها
431
+ if any(line.startswith(indicator) for indicator in ['ماده', 'تبصره']):
432
+ formatted_lines.append(f"\n{line}")
433
+ # قالب‌بندی بندها
434
+ elif any(line.startswith(indicator) for indicator in ['الف)', 'ب)', 'ج)']):
435
+ formatted_lines.append(f" {line}")
436
+ # قالب‌بندی فصول و ابواب
437
+ elif any(line.startswith(indicator) for indicator in ['فصل', 'باب', 'کتاب']):
438
+ formatted_lines.append(f"\n\n{line.upper()}\n")
439
+ else:
440
+ formatted_lines.append(line)
441
+
442
+ return '\n'.join(formatted_lines)
443
+
444
+ def extract_legal_entities(self, text: str) -> Dict[str, List[str]]:
445
+ """استخراج موجودیت‌های حقوقی از متن"""
446
+ entities = {
447
+ 'articles': [],
448
+ 'citations': [],
449
+ 'legal_terms': [],
450
+ 'organizations': [],
451
+ 'laws': []
452
+ }
453
+
454
+ # استخراج مواد و تبصره‌ها
455
+ for pattern in self.citation_patterns:
456
+ matches = re.findall(pattern, text)
457
+ if matches:
458
+ entities['citations'].extend(matches)
459
+
460
+ # استخراج اصطلاحات حقوقی
461
+ for category, terms in PERSIAN_LEGAL_DICTIONARY.items():
462
+ found_terms = [term for term in terms if term in text]
463
+ entities['legal_terms'].extend(found_terms)
464
+
465
+ # استخراج نام قوانین
466
+ law_patterns = [
467
+ r'قانون\s+([^۔\.\n]{5,50})',
468
+ r'آیین‌نامه\s+([^۔\.\n]{5,50})',
469
+ r'مقرره\s+([^۔\.\n]{5,50})'
470
+ ]
471
+
472
+ for pattern in law_patterns:
473
+ matches = re.findall(pattern, text)
474
+ entities['laws'].extend(matches)
475
+
476
+ return entities
477
+
478
+ class AntiDDoSManager:
479
+ """مدیریت ضد حملات DDoS و تنوع درخواست‌ها"""
480
+
481
+ def __init__(self):
482
+ self.request_history = {}
483
+ self.user_agents = [
484
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
485
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
486
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
487
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0',
488
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:89.0) Gecko/20100101 Firefox/89.0'
489
+ ]
490
+
491
+ def get_request_delay(self, domain: str) -> float:
492
+ """محاسبه تأخیر مناسب برای هر منبع"""
493
+ source_info = self._identify_source(domain)
494
+
495
+ if source_info and 'delay_range' in source_info:
496
+ min_delay, max_delay = source_info['delay_range']
497
+ return random.uniform(min_delay, max_delay)
498
+
499
+ # تأخیر پیش‌فرض
500
+ return random.uniform(1, 3)
501
+
502
+ def get_random_headers(self) -> Dict[str, str]:
503
+ """تولید هدرهای تصادفی"""
504
+ return {
505
+ 'User-Agent': random.choice(self.user_agents),
506
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
507
+ 'Accept-Language': random.choice(['fa,en;q=0.9', 'fa-IR,fa;q=0.9,en;q=0.8', 'fa;q=0.9,en;q=0.8']),
508
+ 'Accept-Encoding': 'gzip, deflate',
509
+ 'Connection': 'keep-alive',
510
+ 'Upgrade-Insecure-Requests': '1',
511
+ 'Cache-Control': random.choice(['no-cache', 'max-age=0', 'no-store'])
512
+ }
513
+
514
+ def _identify_source(self, domain: str) -> Optional[Dict]:
515
+ """شناسایی منبع بر اساس دامنه"""
516
+ for source_name, config in LEGAL_SOURCES_CONFIG.items():
517
+ base_domain = config['base_url'].replace('https://', '').replace('http://', '')
518
+ if base_domain in domain:
519
+ return config
520
+ return None
521
+
522
+ def should_allow_request(self, domain: str) -> bool:
523
+ """بررسی اینکه آیا درخواست مجاز است"""
524
+ current_time = time.time()
525
+
526
+ if domain not in self.request_history:
527
+ self.request_history[domain] = []
528
+
529
+ # حذف درخواست‌های قدیمی (بیش از 1 ساعت)
530
+ self.request_history[domain] = [
531
+ req_time for req_time in self.request_history[domain]
532
+ if current_time - req_time < 3600
533
+ ]
534
+
535
+ # بررسی تعداد درخواست‌ها در ساعت گذشته
536
+ if len(self.request_history[domain]) >= 50: # حداکثر 50 درخواست در ساعت
537
+ return False
538
+
539
+ # ثبت درخواست جدید
540
+ self.request_history[domain].append(current_time)
541
+ return True
542
+
543
+ class ModelManager:
544
+ """مدیریت پیشرفته مدل‌های هوش مصنوعی"""
545
+
546
+ def __init__(self):
547
+ self.models = {}
548
+ self.model_status = {}
549
+
550
+ def load_models_progressively(self, progress_callback=None) -> Dict[str, Any]:
551
+ """بارگذاری تدریجی مدل‌ها با مدیریت حافظه"""
552
+ logger.info("🚀 شروع بارگذاری مدل‌های هوش مصنوعی...")
553
+
554
+ if progress_callback:
555
+ progress_callback("آماده‌سازی محیط...", 0.05)
556
+
557
+ # ایجاد دایرکتوری کش
558
+ cache_dir = "/tmp/hf_cache"
559
+ os.makedirs(cache_dir, exist_ok=True)
560
+
561
+ # فاز 1: Tokenizer
562
+ try:
563
+ if MemoryManager.check_memory_available(100):
564
+ logger.info("📝 بارگذاری Tokenizer...")
565
+ if progress_callback:
566
+ progress_callback("بارگذاری Tokenizer...", 0.2)
567
+
568
+ self.models['tokenizer'] = AutoTokenizer.from_pretrained(
569
+ "HooshvareLab/bert-fa-base-uncased",
570
+ cache_dir=cache_dir,
571
+ local_files_only=False
572
+ )
573
+ self.model_status['tokenizer'] = 'loaded'
574
+ logger.info("✅ Tokenizer بارگذاری شد")
575
+ else:
576
+ self.model_status['tokenizer'] = 'memory_insufficient'
577
+ except Exception as e:
578
+ logger.error(f"❌ خطا در بارگذاری Tokenizer: {e}")
579
+ self.model_status['tokenizer'] = 'failed'
580
+
581
+ # فاز 2: مدل طبقه‌بندی متن
582
+ try:
583
+ if MemoryManager.check_memory_available(400):
584
+ logger.info("🏷️ بارگذاری مدل طبقه‌بندی...")
585
+ if progress_callback:
586
+ progress_callback("بارگذاری مدل طبقه‌بندی...", 0.5)
587
+
588
+ self.models['classifier'] = pipeline(
589
+ "text-classification",
590
+ model="HooshvareLab/bert-fa-base-uncased-clf-persiannews",
591
+ tokenizer="HooshvareLab/bert-fa-base-uncased-clf-persiannews",
592
+ device=-1,
593
+ return_all_scores=True,
594
+ model_kwargs={"cache_dir": cache_dir}
595
+ )
596
+ self.model_status['classifier'] = 'loaded'
597
+ logger.info("✅ مدل طبقه‌بندی بارگذاری شد")
598
+ else:
599
+ self.model_status['classifier'] = 'memory_insufficient'
600
+ except Exception as e:
601
+ logger.error(f"❌ خطا در بارگذاری مدل طبقه‌بندی: {e}")
602
+ self.model_status['classifier'] = 'failed'
603
+
604
+ # فاز 3: مدل تشخیص موجودیت
605
+ try:
606
+ if MemoryManager.check_memory_available(500):
607
+ logger.info("👤 بارگذاری مدل NER...")
608
+ if progress_callback:
609
+ progress_callback("بارگذاری مدل تشخیص موجودیت...", 0.8)
610
+
611
+ self.models['ner'] = pipeline(
612
+ "ner",
613
+ model="HooshvareLab/bert-fa-base-uncased-ner",
614
+ tokenizer="HooshvareLab/bert-fa-base-uncased-ner",
615
+ device=-1,
616
+ aggregation_strategy="simple",
617
+ model_kwargs={"cache_dir": cache_dir}
618
+ )
619
+ self.model_status['ner'] = 'loaded'
620
+ logger.info("✅ مدل NER بارگذاری شد")
621
+ else:
622
+ self.model_status['ner'] = 'memory_insufficient'
623
+ except Exception as e:
624
+ logger.error(f"❌ خطا در بارگذاری مدل NER: {e}")
625
+ self.model_status['ner'] = 'failed'
626
+
627
+ if progress_callback:
628
+ progress_callback("تکمیل بارگذاری مدل‌ها", 1.0)
629
+
630
+ # پاکسازی حافظه
631
+ MemoryManager.cleanup_memory()
632
+
633
+ loaded_count = sum(1 for status in self.model_status.values() if status == 'loaded')
634
+ logger.info(f"🎯 {loaded_count} مدل با موفقیت بارگذاری شد")
635
+
636
+ return self.models
637
+
638
+ def get_model_status(self) -> str:
639
+ """دریافت وضعیت مدل‌ها"""
640
+ status_lines = []
641
+ status_icons = {
642
+ 'loaded': '✅',
643
+ 'failed': '❌',
644
+ 'memory_insufficient': '⚠️',
645
+ 'not_loaded': '⏳'
646
+ }
647
+
648
+ for model_name, status in self.model_status.items():
649
+ icon = status_icons.get(status, '❓')
650
+ status_persian = {
651
+ 'loaded': 'بارگذاری شده',
652
+ 'failed': 'خطا در بارگذاری',
653
+ 'memory_insufficient': 'حافظه ناکافی',
654
+ 'not_loaded': 'بارگذاری نشده'
655
+ }.get(status, 'نامشخص')
656
+
657
+ status_lines.append(f"{icon} {model_name}: {status_persian}")
658
+
659
+ memory_usage = MemoryManager.get_memory_usage()
660
+ status_lines.append(f"\n💾 مصرف حافظه: {memory_usage:.1f} MB")
661
+
662
+ return '\n'.join(status_lines)
663
+
664
+ class LegalDocumentScraper:
665
+ """استخراج‌کننده پیشرفته اسناد حقوقی"""
666
+
667
+ def __init__(self, model_manager: ModelManager, text_processor: SmartTextProcessor):
668
+ self.model_manager = model_manager
669
+ self.text_processor = text_processor
670
+ self.anti_ddos = AntiDDoSManager()
671
+ self.session = self._create_session()
672
+
673
+ def _create_session(self) -> requests.Session:
674
+ """ایجاد session با تنظیمات بهینه"""
675
+ session = requests.Session()
676
+
677
+ # تنظیم adapter برای retry
678
+ from requests.adapters import HTTPAdapter
679
+ from urllib3.util.retry import Retry
680
+
681
+ retry_strategy = Retry(
682
+ total=3,
683
+ backoff_factor=1,
684
+ status_forcelist=[429, 500, 502, 503, 504]
685
+ )
686
+ adapter = HTTPAdapter(max_retries=retry_strategy)
687
+ session.mount("http://", adapter)
688
+ session.mount("https://", adapter)
689
+
690
+ return session
691
+
692
+ def scrape_legal_source(self, url: str, progress_callback=None) -> Dict[str, Any]:
693
+ """استخراج هوشمند سند حقوقی"""
694
+ try:
695
+ domain = urlparse(url).netloc
696
+
697
+ # بررسی مجوز
698
+ if not self.anti_ddos.should_allow_request(domain):
699
+ raise Exception("تعداد درخواست‌ها از حد مجاز تجاوز کرده است")
700
+
701
+ if progress_callback:
702
+ progress_callback(f"🔗 اتصال به {domain}...", 0.1)
703
+
704
+ # اعمال تأخیر ضد DDoS
705
+ delay = self.anti_ddos.get_request_delay(domain)
706
+ time.sleep(delay)
707
+
708
+ # درخواست اصلی
709
+ headers = self.anti_ddos.get_random_headers()
710
+ response = self.session.get(url, headers=headers, timeout=30, allow_redirects=True)
711
+ response.raise_for_status()
712
+ response.encoding = 'utf-8'
713
+
714
+ if progress_callback:
715
+ progress_callback("📄 تجزیه محتوای HTML...", 0.3)
716
+
717
+ # تجزیه HTML
718
+ soup = BeautifulSoup(response.content, 'html.parser')
719
+ self._clean_soup(soup)
720
+
721
+ # شناسایی منبع
722
+ source_info = self._identify_legal_source(url)
723
+
724
+ if progress_callback:
725
+ progress_callback("🎯 استخراج محتوای هدفمند...", 0.5)
726
+
727
+ # استخراج محتوا
728
+ content_fragments = self._extract_content_intelligently(soup, source_info)
729
+
730
+ # بازسازی متن
731
+ reconstructed_text = self.text_processor.reconstruct_legal_text(content_fragments)
732
+
733
+ if not reconstructed_text or len(reconstructed_text.strip()) < 50:
734
+ raise Exception("محتوای کافی استخراج نشد")
735
+
736
+ if progress_callback:
737
+ progress_callback("🔧 تحلیل و پردازش متن...", 0.7)
738
+
739
+ # تحلیل‌های پیشرفته
740
+ quality_assessment = self._assess_content_quality(reconstructed_text)
741
+ legal_entities = self.text_processor.extract_legal_entities(reconstructed_text)
742
+ long_sentences = self.text_processor.detect_long_sentences(reconstructed_text)
743
+ ai_analysis = self._apply_ai_analysis(reconstructed_text)
744
+
745
+ # نتیجه نهایی
746
+ result = {
747
+ 'url': url,
748
+ 'source_info': source_info,
749
+ 'title': self._extract_title(soup),
750
+ 'content': reconstructed_text,
751
+ 'word_count': len(reconstructed_text.split()),
752
+ 'character_count': len(reconstructed_text),
753
+ 'quality_assessment': quality_assessment,
754
+ 'legal_entities': legal_entities,
755
+ 'long_sentences': long_sentences,
756
+ 'ai_analysis': ai_analysis,
757
+ 'extraction_metadata': {
758
+ 'method': 'advanced_extraction',
759
+ 'fragments_count': len(content_fragments),
760
+ 'response_size': len(response.content),
761
+ 'encoding': response.encoding,
762
+ 'anti_ddos_delay': delay
763
+ },
764
+ 'timestamp': datetime.now().isoformat(),
765
+ 'status': 'موفق'
766
+ }
767
+
768
+ if progress_callback:
769
+ progress_callback("✅ استخراج با موفقیت تکمیل شد", 1.0)
770
+
771
+ return result
772
+
773
+ except requests.RequestException as e:
774
+ return self._create_error_result(url, f"خطای شبکه: {str(e)}")
775
+ except Exception as e:
776
+ return self._create_error_result(url, f"خطای پردازش: {str(e)}")
777
+
778
+ def _identify_legal_source(self, url: str) -> Dict[str, Any]:
779
+ """شناسایی منبع حقوقی"""
780
+ domain = urlparse(url).netloc
781
+
782
+ for source_name, config in LEGAL_SOURCES_CONFIG.items():
783
+ if config['base_url'].replace('https://', '').replace('http://', '') in domain:
784
+ return {
785
+ 'name': source_name,
786
+ 'type': 'official',
787
+ 'credibility': 'high',
788
+ 'config': config
789
+ }
790
+
791
+ # بررسی الگوهای عمومی سایت‌های حقوقی
792
+ legal_indicators = ['law', 'legal', 'court', 'judiciary', 'قانون', 'حقوق', 'دادگاه']
793
+ if any(indicator in domain.lower() for indicator in legal_indicators):
794
+ return {
795
+ 'name': 'منبع حقوقی شناخته نشده',
796
+ 'type': 'legal_related',
797
+ 'credibility': 'medium',
798
+ 'config': {'max_depth': 1, 'delay_range': (2, 4)}
799
+ }
800
+
801
+ return {
802
+ 'name': 'منبع عمومی',
803
+ 'type': 'general',
804
+ 'credibility': 'low',
805
+ 'config': {'max_depth': 0, 'delay_range': (3, 6)}
806
+ }
807
+
808
+ def _clean_soup(self, soup: BeautifulSoup) -> None:
809
+ """پاکسازی پیشرفته HTML"""
810
+ # حذف عناصر غیرضروری
811
+ unwanted_tags = [
812
+ 'script', 'style', 'nav', 'footer', 'header', 'aside',
813
+ 'advertisement', 'ads', 'sidebar', 'menu', 'breadcrumb',
814
+ 'social', 'share', 'comment', 'popup', 'modal'
815
+ ]
816
+
817
+ for tag in unwanted_tags:
818
+ for element in soup.find_all(tag):
819
+ element.decompose()
820
+
821
+ # حذف عناصر با کلاس‌های مشخص
822
+ unwanted_classes = [
823
+ 'ad', 'ads', 'advertisement', 'sidebar', 'menu', 'nav',
824
+ 'footer', 'header', 'social', 'share', 'comment', 'popup'
825
+ ]
826
+
827
+ for class_name in unwanted_classes:
828
+ for element in soup.find_all(class_=lambda x: x and class_name in ' '.join(x).lower()):
829
+ element.decompose()
830
+
831
+ def _extract_content_intelligently(self, soup: BeautifulSoup, source_info: Dict) -> List[str]:
832
+ """استخراج هوشمند محتوا"""
833
+ fragments = []
834
+
835
+ # استراتژی 1: استفاده از تنظیمات منبع شناخته شده
836
+ if source_info.get('config') and source_info['config'].get('selectors'):
837
+ for selector in source_info['config']['selectors']:
838
+ elements = soup.select(selector)
839
+ for element in elements:
840
+ text = self._extract_clean_text(element)
841
+ if self._is_valid_content(text):
842
+ fragments.append(text)
843
+
844
+ # استراتژی 2: جستجوی محتوای اصلی
845
+ if not fragments:
846
+ main_selectors = [
847
+ 'main', 'article', '.main-content', '.content', '.post-content',
848
+ '.article-content', '.news-content', '.law-content', '.legal-text',
849
+ '#main', '#content', '#article', '.document-content'
850
+ ]
851
+
852
+ for selector in main_selectors:
853
+ elements = soup.select(selector)
854
+ for element in elements:
855
+ text = self._extract_clean_text(element)
856
+ if self._is_valid_content(text):
857
+ fragments.append(text)
858
+ if fragments:
859
+ break
860
+
861
+ # استراتژی 3: استخراج از پاراگراف‌ها
862
+ if not fragments:
863
+ for tag in ['p', 'div', 'section', 'article']:
864
+ elements = soup.find_all(tag)
865
+ for element in elements:
866
+ text = self._extract_clean_text(element)
867
+ if self._is_valid_content(text, min_length=30):
868
+ fragments.append(text)
869
+
870
+ # استراتژی 4: fallback به body
871
+ if not fragments:
872
+ body = soup.find('body')
873
+ if body:
874
+ text = self._extract_clean_text(body)
875
+ if text:
876
+ paragraphs = [p.strip() for p in text.split('\n') if p.strip()]
877
+ fragments.extend(p for p in paragraphs if self._is_valid_content(p, min_length=20))
878
+
879
+ return fragments
880
+
881
+ def _extract_clean_text(self, element) -> str:
882
+ """استخراج متن تمیز"""
883
+ if not element:
884
+ return ""
885
+
886
+ # حذف عناصر تودرتو غیرضروری
887
+ for unwanted in element.find_all(['script', 'style', 'nav', 'aside']):
888
+ unwanted.decompose()
889
+
890
+ text = element.get_text(separator=' ', strip=True)
891
+ text = re.sub(r'\s+', ' ', text)
892
+
893
+ return text.strip()
894
+
895
+ def _is_valid_content(self, text: str, min_length: int = 50) -> bool:
896
+ """بررسی اعتبار محتوا"""
897
+ if not text or len(text) < min_length:
898
+ return False
899
+
900
+ # بررسی نسبت کاراکترهای فارسی
901
+ persian_chars = sum(1 for c in text if '\u0600' <= c <= '\u06FF')
902
+ persian_ratio = persian_chars / len(text) if text else 0
903
+
904
+ if persian_ratio < 0.2:
905
+ return False
906
+
907
+ # بررسی کلمات کلیدی حقوقی
908
+ legal_keywords = ['قانون', 'ماده', 'تبصره', 'مقرره', 'آیین‌نامه', 'دادگاه', 'حکم', 'رای']
909
+ has_legal_content = any(keyword in text for keyword in legal_keywords)
910
+
911
+ return has_legal_content or persian_ratio > 0.5
912
+
913
+ def _extract_title(self, soup: BeautifulSoup) -> str:
914
+ """استخراج عنوان"""
915
+ title_selectors = [
916
+ 'h1', 'h2', 'title', '.page-title', '.article-title',
917
+ '.post-title', '.document-title', '.news-title', '.law-title'
918
+ ]
919
+
920
+ for selector in title_selectors:
921
+ element = soup.select_one(selector)
922
+ if element:
923
+ title = element.get_text(strip=True)
924
+ if title and 5 < len(title) < 200:
925
+ return re.sub(r'\s+', ' ', title)
926
+
927
+ return "بدون عنوان"
928
+
929
+ def _assess_content_quality(self, text: str) -> Dict[str, Any]:
930
+ """ارزیابی کیفیت محتوا"""
931
+ assessment = {
932
+ 'overall_score': 0,
933
+ 'factors': {},
934
+ 'issues': [],
935
+ 'strengths': []
936
+ }
937
+
938
+ if not text:
939
+ assessment['issues'].append('متن خالی')
940
+ return assessment
941
+
942
+ factors = {}
943
+
944
+ # طول متن
945
+ word_count = len(text.split())
946
+ if word_count >= 100:
947
+ factors['length'] = min(100, word_count / 10)
948
+ assessment['strengths'].append('طول مناسب متن')
949
+ else:
950
+ factors['length'] = word_count
951
+ assessment['issues'].append('متن کوتاه')
952
+
953
+ # نسبت فارسی
954
+ persian_chars = sum(1 for c in text if '\u0600' <= c <= '\u06FF')
955
+ persian_ratio = persian_chars / len(text) if text else 0
956
+ factors['persian_content'] = persian_ratio * 100
957
+
958
+ if persian_ratio >= 0.7:
959
+ assessment['strengths'].append('محتوای فارسی غنی')
960
+ elif persian_ratio < 0.3:
961
+ assessment['issues'].append('محتوای فارسی کم')
962
+
963
+ # محتوای حقوقی
964
+ legal_terms_count = 0
965
+ for category, terms in PERSIAN_LEGAL_DICTIONARY.items():
966
+ legal_terms_count += sum(1 for term in terms if term in text)
967
+
968
+ factors['legal_content'] = min(100, legal_terms_count * 5)
969
+
970
+ if legal_terms_count >= 10:
971
+ assessment['strengths'].append('غنی از اصطلاحات حقوقی')
972
+ elif legal_terms_count < 3:
973
+ assessment['issues'].append('فقر اصطلاحات حقوقی')
974
+
975
+ # ساختار متن
976
+ structure_score = 0
977
+ if 'ماده' in text:
978
+ structure_score += 20
979
+ if any(indicator in text for indicator in ['تبصره', 'بند', 'فصل']):
980
+ structure_score += 15
981
+ if re.search(r'[۰-۹]+', text):
982
+ structure_score += 10
983
+
984
+ factors['structure'] = structure_score
985
+
986
+ if structure_score >= 30:
987
+ assessment['strengths'].append('ساختار منظم حقوقی')
988
+
989
+ # محاسبه امتیاز کلی
990
+ assessment['factors'] = factors
991
+ assessment['overall_score'] = sum(factors.values()) / len(factors) if factors else 0
992
+ assessment['overall_score'] = min(100, max(0, assessment['overall_score']))
993
+
994
+ return assessment
995
+
996
+ def _apply_ai_analysis(self, text: str) -> Dict[str, Any]:
997
+ """اعمال تحلیل هوش مصنوعی"""
998
+ analysis = {
999
+ 'classification': None,
1000
+ 'entities': [],
1001
+ 'confidence_scores': {},
1002
+ 'model_performance': {}
1003
+ }
1004
+
1005
+ if not text or len(text.split()) < 10:
1006
+ return analysis
1007
+
1008
+ # آماده‌سازی متن
1009
+ text_sample = ' '.join(text.split()[:300])
1010
+
1011
+ # طبقه‌بندی متن
1012
+ if 'classifier' in self.model_manager.models:
1013
+ try:
1014
+ start_time = time.time()
1015
+ classification_result = self.model_manager.models['classifier'](text_sample)
1016
+
1017
+ if classification_result:
1018
+ analysis['classification'] = classification_result[:3]
1019
+ analysis['confidence_scores']['classification'] = classification_result[0]['score']
1020
+
1021
+ analysis['model_performance']['classification_time'] = time.time() - start_time
1022
+
1023
+ except Exception as e:
1024
+ logger.error(f"خطا در طبقه‌بندی: {e}")
1025
+
1026
+ # تشخیص موجودیت
1027
+ if 'ner' in self.model_manager.models:
1028
+ try:
1029
+ start_time = time.time()
1030
+ ner_result = self.model_manager.models['ner'](text_sample[:1000])
1031
+
1032
+ if ner_result:
1033
+ high_confidence_entities = [
1034
+ entity for entity in ner_result
1035
+ if entity.get('score', 0) > 0.5
1036
+ ]
1037
+ analysis['entities'] = high_confidence_entities[:15]
1038
+
1039
+ analysis['model_performance']['ner_time'] = time.time() - start_time
1040
+
1041
+ except Exception as e:
1042
+ logger.error(f"خطا در تشخیص موجودیت: {e}")
1043
+
1044
+ return analysis
1045
+
1046
+ def _create_error_result(self, url: str, error_message: str) -> Dict[str, Any]:
1047
+ """ایجاد نتیجه خطا"""
1048
+ return {
1049
+ 'url': url,
1050
+ 'error': error_message,
1051
+ 'status': 'ناموفق',
1052
+ 'timestamp': datetime.now().isoformat(),
1053
+ 'content': '',
1054
+ 'word_count': 0,
1055
+ 'quality_assessment': {'overall_score': 0, 'issues': [error_message]}
1056
+ }
1057
+
1058
+ class PersianLegalScraperApp:
1059
+ """اپلیکیشن اصلی با رابط کاربری پیشرفته"""
1060
+
1061
+ def __init__(self):
1062
+ self.text_processor = SmartTextProcessor()
1063
+ self.model_manager = ModelManager()
1064
+ self.scraper = None
1065
+ self.results = []
1066
+ self.processing_stats = {
1067
+ 'total_processed': 0,
1068
+ 'successful': 0,
1069
+ 'failed': 0,
1070
+ 'total_words': 0
1071
+ }
1072
+
1073
+ # مقداردهی اولیه
1074
+ self._initialize_system()
1075
+
1076
+ def _initialize_system(self):
1077
+ """مقداردهی سیستم"""
1078
+ try:
1079
+ logger.info("🚀 مقداردهی سیستم...")
1080
+ self.model_manager.load_models_progressively()
1081
+ self.scraper = LegalDocumentScraper(self.model_manager, self.text_processor)
1082
+ logger.info("✅ سیستم آماده است")
1083
+ except Exception as e:
1084
+ logger.error(f"❌ خطا در مقداردهی: {e}")
1085
+
1086
+ def process_single_url(self, url: str, progress=gr.Progress()) -> Tuple[str, str, str]:
1087
+ """پردازش یک URL"""
1088
+ if not url or not url.strip():
1089
+ return "❌ خطا: آدرس معتبری وارد کنید", "", ""
1090
+
1091
+ url = url.strip()
1092
+ if not url.startswith(('http://', 'https://')):
1093
+ url = 'https://' + url
1094
+
1095
+ try:
1096
+ progress(0.0, desc="شروع پردازش...")
1097
+
1098
+ def progress_callback(message: str, value: float):
1099
+ progress(value, desc=message)
1100
+
1101
+ result = self.scraper.scrape_legal_source(url, progress_callback)
1102
+
1103
+ if result.get('status') == 'موفق':
1104
+ # به‌روزرسانی آمار
1105
+ self.processing_stats['total_processed'] += 1
1106
+ self.processing_stats['successful'] += 1
1107
+ self.processing_stats['total_words'] += result.get('word_count', 0)
1108
+
1109
+ # ذخیره نتیجه
1110
+ self.results.append(result)
1111
+
1112
+ # تنظیم خروجی‌ها
1113
+ status_text = self._format_single_result(result)
1114
+ analysis_text = self._format_analysis_result(result)
1115
+ content_text = result.get('content', '')
1116
+
1117
+ return status_text, content_text, analysis_text
1118
+ else:
1119
+ self.processing_stats['total_processed'] += 1
1120
+ self.processing_stats['failed'] += 1
1121
+ error_msg = result.get('error', 'خطای نامشخص')
1122
+ return f"❌ خطا: {error_msg}", "", ""
1123
+
1124
+ except Exception as e:
1125
+ self.processing_stats['total_processed'] += 1
1126
+ self.processing_stats['failed'] += 1
1127
+ logger.error(f"خطا در پردازش: {e}")
1128
+ return f"❌ خطای سیستمی: {str(e)}", "", ""
1129
+
1130
+ def _format_single_result(self, result: Dict[str, Any]) -> str:
1131
+ """قالب‌بندی نتیجه با آیکون‌های SVG"""
1132
+ quality = result.get('quality_assessment', {})
1133
+ source_info = result.get('source_info', {})
1134
+ legal_entities = result.get('legal_entities', {})
1135
+ long_sentences = result.get('long_sentences', [])
1136
+
1137
+ lines = [
1138
+ f"{SVG_ICONS['success']} **وضعیت**: {result.get('status')}",
1139
+ f"{SVG_ICONS['document']} **عنوان**: {result.get('title', 'بدون عنوان')}",
1140
+ f"🏛️ **منبع**: {source_info.get('name', 'نامشخص')} ({source_info.get('credibility', 'نامشخص')})",
1141
+ f"📊 **آمار محتوا**: {result.get('word_count', 0):,} کلمه، {result.get('character_count', 0):,} کاراکتر",
1142
+ f"🎯 **کیفیت کلی**: {quality.get('overall_score', 0):.1f}/100",
1143
+ f"📈 **محتوای فارسی**: {quality.get('factors', {}).get('persian_content', 0):.1f}%",
1144
+ f"⚖️ **محتوای حقوقی**: {quality.get('factors', {}).get('legal_content', 0):.1f}/100",
1145
+ f"📚 **ارجاعات حقوقی**: {len(legal_entities.get('citations', []))} مورد",
1146
+ f"🏷️ **اصطلاحات حقوقی**: {len(legal_entities.get('legal_terms', []))} مورد"
1147
+ ]
1148
+
1149
+ # جملات طولانی
1150
+ if long_sentences:
1151
+ lines.append(f"{SVG_ICONS['warning']} **جملات طولانی**: {len(long_sentences)} مورد")
1152
+
1153
+ # نقاط قوت
1154
+ strengths = quality.get('strengths', [])
1155
+ if strengths:
1156
+ lines.append(f"\n✨ **نقاط قوت**: {' | '.join(strengths)}")
1157
+
1158
+ # مشکلات
1159
+ issues = quality.get('issues', [])
1160
+ if issues:
1161
+ lines.append(f"{SVG_ICONS['warning']} **نکات**: {' | '.join(issues)}")
1162
+
1163
+ lines.append(f"🕐 **زمان**: {result.get('timestamp', '')[:19]}")
1164
+
1165
+ return '\n'.join(lines)
1166
+
1167
+ def _format_analysis_result(self, result: Dict[str, Any]) -> str:
1168
+ """قالب‌بندی تحلیل هوش مصنوعی"""
1169
+ ai_analysis = result.get('ai_analysis', {})
1170
+ legal_entities = result.get('legal_entities', {})
1171
+ long_sentences = result.get('long_sentences', [])
1172
+
1173
+ lines = [
1174
+ f"{SVG_ICONS['analyze']} **تحلیل هوش مصنوعی**\n",
1175
+ f"📊 **وضعیت مدل‌ها**: {self.model_manager.get_model_status()}\n"
1176
+ ]
1177
+
1178
+ # طبقه‌بندی
1179
+ classification = ai_analysis.get('classification')
1180
+ if classification:
1181
+ lines.append("🏷️ **طبقه‌بندی محتوا**:")
1182
+ for i, item in enumerate(classification[:3], 1):
1183
+ label = item.get('label', 'نامشخص')
1184
+ score = item.get('score', 0)
1185
+ lines.append(f"{i}. {label}: {score:.1%}")
1186
+
1187
+ # موجودیت‌های شناسایی شده
1188
+ entities = ai_analysis.get('entities', [])
1189
+ if entities:
1190
+ lines.append("\n👥 **موجودیت‌های شناسایی شده**:")
1191
+ for entity in entities[:8]: # 8 مورد اول
1192
+ word = entity.get('word', '')
1193
+ label = entity.get('entity_group', '')
1194
+ score = entity.get('score', 0)
1195
+ lines.append(f"• {word} ({label}): {score:.1%}")
1196
+
1197
+ # ارجاعات حقوقی
1198
+ citations = legal_entities.get('citations', [])
1199
+ if citations:
1200
+ lines.append(f"\n📚 **ارجاعات حقوقی**: {len(citations)} مورد")
1201
+ unique_citations = list(set(citations))[:10]
1202
+ lines.append(f"نمونه: {', '.join(unique_citations)}")
1203
+
1204
+ # قوانین شناسایی شده
1205
+ laws = legal_entities.get('laws', [])
1206
+ if laws:
1207
+ lines.append(f"\n⚖️ **قوانین شناسایی شده**: {len(laws)} مورد")
1208
+ for law in laws[:3]:
1209
+ lines.append(f"• {law}")
1210
+
1211
+ # جملات طولانی
1212
+ if long_sentences:
1213
+ lines.append(f"\n{SVG_ICONS['warning']} **جملات طولانی شناسایی شده**:")
1214
+ for i, sentence_info in enumerate(long_sentences[:3], 1):
1215
+ word_count = sentence_info.get('word_count', 0)
1216
+ suggestions = sentence_info.get('suggestions', [])
1217
+ lines.append(f"{i}. {word_count} کلمه - {', '.join(suggestions[:2])}")
1218
+
1219
+ # عملکرد مدل
1220
+ performance = ai_analysis.get('model_performance', {})
1221
+ if performance:
1222
+ lines.append(f"\n⏱️ **عملکرد**: ")
1223
+ if 'classification_time' in performance:
1224
+ lines.append(f"طبقه‌بندی: {performance['classification_time']:.2f}s")
1225
+ if 'ner_time' in performance:
1226
+ lines.append(f"تشخیص موجودیت: {performance['ner_time']:.2f}s")
1227
+
1228
+ return '\n'.join(lines)
1229
+
1230
+ def process_multiple_urls(self, urls_text: str, progress=gr.Progress()) -> Tuple[str, str]:
1231
+ """پردازش چندین URL"""
1232
+ if not urls_text or not urls_text.strip():
1233
+ return "❌ لطفا لیست آدرس‌ها را وارد کنید", ""
1234
+
1235
+ urls = [url.strip() for url in urls_text.split('\n') if url.strip()]
1236
+ if not urls:
1237
+ return "❌ آدرس معتبری یافت نشد", ""
1238
+
1239
+ # محدودیت تعداد برای HF Spaces
1240
+ if len(urls) > 10:
1241
+ urls = urls[:10]
1242
+ warning_msg = f"{SVG_ICONS['warning']} به دلیل محدودیت‌ها، تنها 10 آدرس اول پردازش می‌شود.\n\n"
1243
+ else:
1244
+ warning_msg = ""
1245
+
1246
+ results = []
1247
+ total_urls = len(urls)
1248
+
1249
+ try:
1250
+ progress(0.0, desc=f"شروع پردازش {total_urls} آدرس...")
1251
+
1252
+ for i, url in enumerate(urls):
1253
+ if not url.startswith(('http://', 'https://')):
1254
+ url = 'https://' + url
1255
+
1256
+ progress_value = i / total_urls
1257
+ progress(progress_value, desc=f"پردازش {i+1} از {total_urls}: {url[:50]}...")
1258
+
1259
+ def progress_callback(message: str, value: float):
1260
+ overall_progress = progress_value + (value * (1/total_urls))
1261
+ progress(overall_progress, desc=f"{message} ({i+1}/{total_urls})")
1262
+
1263
+ result = self.scraper.scrape_legal_source(url, progress_callback)
1264
+ results.append(result)
1265
+
1266
+ # به‌روزرسانی آمار
1267
+ self.processing_stats['total_processed'] += 1
1268
+ if result.get('status') == 'موفق':
1269
+ self.processing_stats['successful'] += 1
1270
+ self.processing_stats['total_words'] += result.get('word_count', 0)
1271
+ else:
1272
+ self.processing_stats['failed'] += 1
1273
+
1274
+ # پاکسازی حافظه هر 3 درخواست
1275
+ if (i + 1) % 3 == 0:
1276
+ MemoryManager.cleanup_memory()
1277
+ time.sleep(1) # کمی استراحت
1278
+
1279
+ progress(1.0, desc="تکمیل پردازش")
1280
+
1281
+ # ذخیره نتایج
1282
+ self.results.extend(results)
1283
+
1284
+ # تنظیم خروجی‌ها
1285
+ summary_text = warning_msg + self._format_batch_summary(results)
1286
+ detailed_results = self._format_batch_details(results)
1287
+
1288
+ return summary_text, detailed_results
1289
+
1290
+ except Exception as e:
1291
+ logger.error(f"خطا در پردازش دسته‌ای: {e}")
1292
+ return f"❌ خطای سیستمی: {str(e)}", ""
1293
+
1294
+ def _format_batch_summary(self, results: List[Dict[str, Any]]) -> str:
1295
+ """خلاصه پردازش دسته‌ای با آیکون‌ها"""
1296
+ total = len(results)
1297
+ successful = sum(1 for r in results if r.get('status') == 'موفق')
1298
+ failed = total - successful
1299
+
1300
+ if successful > 0:
1301
+ total_words = sum(r.get('word_count', 0) for r in results if r.get('status') == 'موفق')
1302
+ avg_quality = sum(r.get('quality_assessment', {}).get('overall_score', 0)
1303
+ for r in results if r.get('status') == 'موفق') / successful
1304
+ else:
1305
+ total_words = 0
1306
+ avg_quality = 0
1307
+
1308
+ lines = [
1309
+ f"{SVG_ICONS['analyze']} **خلاصه پردازش دسته‌ای**",
1310
+ f"📈 **کل آدرس‌ها**: {total}",
1311
+ f"{SVG_ICONS['success']} **موفق**: {successful} ({successful/total*100:.1f}%)",
1312
+ f"{SVG_ICONS['error']} **ناموفق**: {failed} ({failed/total*100:.1f}%)",
1313
+ f"📝 **کل کلمات**: {total_words:,}",
1314
+ f"🎯 **میانگین کیفیت**: {avg_quality:.1f}/100",
1315
+ f"💾 **حافظه**: {MemoryManager.get_memory_usage():.1f} MB",
1316
+ f"🕐 **زمان**: {datetime.now().strftime('%H:%M:%S')}"
1317
+ ]
1318
+
1319
+ return '\n'.join(lines)
1320
+
1321
+ def _format_batch_details(self, results: List[Dict[str, Any]]) -> str:
1322
+ """جزئیات نتایج دسته‌ای"""
1323
+ lines = []
1324
+
1325
+ for i, result in enumerate(results, 1):
1326
+ url = result.get('url', '')
1327
+ status = result.get('status', '')
1328
+
1329
+ if status == 'موفق':
1330
+ title = result.get('title', 'بدون عنوان')
1331
+ word_count = result.get('word_count', 0)
1332
+ quality = result.get('quality_assessment', {}).get('overall_score', 0)
1333
+ source = result.get('source_info', {}).get('name', 'نامشخص')
1334
+
1335
+ lines.extend([
1336
+ f"\n**{i}. {SVG_ICONS['success']} {title}**",
1337
+ f"{SVG_ICONS['link']} {url[:70]}{'...' if len(url) > 70 else ''}",
1338
+ f"🏛️ منبع: {source}",
1339
+ f"📊 {word_count:,} کلمه | کیفیت: {quality:.1f}/100"
1340
+ ])
1341
+ else:
1342
+ error = result.get('error', 'خطای نامشخص')
1343
+ lines.extend([
1344
+ f"\n**{i}. {SVG_ICONS['error']} ناموفق**",
1345
+ f"{SVG_ICONS['link']} {url[:70]}{'...' if len(url) > 70 else ''}",
1346
+ f"❗ {error[:80]}{'...' if len(error) > 80 else ''}"
1347
+ ])
1348
+
1349
+ return '\n'.join(lines)
1350
+
1351
+ def export_results(self) -> Tuple[str, Optional[str]]:
1352
+ """صادرات نتایج"""
1353
+ if not self.results:
1354
+ return f"{SVG_ICONS['error']} نتیجه‌ای برای صادرات وجود ندارد", None
1355
+
1356
+ try:
1357
+ successful_results = [r for r in self.results if r.get('status') == 'موفق']
1358
+
1359
+ if not successful_results:
1360
+ return f"{SVG_ICONS['error']} نتیجه موفقی برای صادرات وجود ندارد", None
1361
+
1362
+ export_data = []
1363
+ for result in successful_results:
1364
+ quality = result.get('quality_assessment', {})
1365
+ source_info = result.get('source_info', {})
1366
+ ai_analysis = result.get('ai_analysis', {})
1367
+
1368
+ # استخراج اطلاعات طبقه‌بندی
1369
+ classification = ai_analysis.get('classification', [])
1370
+ top_class = classification[0].get('label', '') if classification else ''
1371
+
1372
+ export_data.append({
1373
+ 'آدرس': result.get('url', ''),
1374
+ 'عنوان': result.get('title', ''),
1375
+ 'منبع': source_info.get('name', ''),
1376
+ 'اعتبار منبع': source_info.get('credibility', ''),
1377
+ 'تعداد کلمات': result.get('word_count', 0),
1378
+ 'کیفیت کلی': round(quality.get('overall_score', 0), 1),
1379
+ 'محتوای فارسی (%)': round(quality.get('factors', {}).get('persian_content', 0), 1),
1380
+ 'محتوای حقوقی': round(quality.get('factors', {}).get('legal_content', 0), 1),
1381
+ 'طبقه‌بندی AI': top_class,
1382
+ 'زمان استخراج': result.get('timestamp', ''),
1383
+ 'محتوا': result.get('content', '')[:2000] + '...' if len(result.get('content', '')) > 2000 else result.get('content', '')
1384
+ })
1385
+
1386
+ df = pd.DataFrame(export_data)
1387
+
1388
+ # ذخیره فایل
1389
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
1390
+ csv_path = f"/tmp/legal_scraping_results_{timestamp}.csv"
1391
+ df.to_csv(csv_path, index=False, encoding='utf-8-sig')
1392
+
1393
+ summary = f"{SVG_ICONS['export']} {len(export_data)} سند با موفقیت صادر شد"
1394
+ return summary, csv_path
1395
+
1396
+ except Exception as e:
1397
+ logger.error(f"خطا در صادرات: {e}")
1398
+ return f"{SVG_ICONS['error']} خطا در صادرات: {str(e)}", None
1399
+
1400
+ def get_system_status(self) -> str:
1401
+ """وضعیت سیستم"""
1402
+ model_status = self.model_manager.get_model_status()
1403
+ memory_usage = MemoryManager.get_memory_usage()
1404
+
1405
+ lines = [
1406
+ f"{SVG_ICONS['settings']} **وضعیت سیستم**\n",
1407
+ f"💾 **حافظه**: {memory_usage:.1f} MB",
1408
+ f"📊 **آمار کلی**:",
1409
+ f" • کل پردازش شده: {self.processing_stats['total_processed']}",
1410
+ f" • موفق: {self.processing_stats['successful']}",
1411
+ f" • ناموفق: {self.processing_stats['failed']}",
1412
+ f" • کل کلمات: {self.processing_stats['total_words']:,}",
1413
+ f"\n🤖 **مدل‌ها**:",
1414
+ model_status,
1415
+ f"\n⏰ **آخرین بروزرسانی**: {datetime.now().strftime('%Y/%m/%d %H:%M:%S')}"
1416
+ ]
1417
+
1418
+ return '\n'.join(lines)
1419
+
1420
+ def clear_results(self) -> str:
1421
+ """پاکسازی نتایج و حافظه"""
1422
+ self.results.clear()
1423
+ self.processing_stats = {
1424
+ 'total_processed': 0,
1425
+ 'successful': 0,
1426
+ 'failed': 0,
1427
+ 'total_words': 0
1428
+ }
1429
+ MemoryManager.cleanup_memory()
1430
+ return f"{SVG_ICONS['success']} نتایج و حافظه پاکسازی شد"
1431
+
1432
+ def create_interface(self):
1433
+ """ایجاد رابط کاربری Gradio"""
1434
+
1435
+ # CSS سفارشی برای RTL و فونت فارسی
1436
+ custom_css = """
1437
+ .rtl {
1438
+ direction: rtl;
1439
+ text-align: right;
1440
+ font-family: 'Vazirmatn', 'Tahoma', sans-serif;
1441
+ }
1442
+
1443
+ .persian-title {
1444
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1445
+ color: white;
1446
+ padding: 20px;
1447
+ border-radius: 10px;
1448
+ text-align: center;
1449
+ margin-bottom: 20px;
1450
+ font-family: 'Vazirmatn', 'Tahoma', sans-serif;
1451
+ }
1452
+
1453
+ .status-box {
1454
+ border: 1px solid #ddd;
1455
+ border-radius: 8px;
1456
+ padding: 15px;
1457
+ background-color: #f9f9f9;
1458
+ direction: rtl;
1459
+ font-family: 'Vazirmatn', 'Tahoma', sans-serif;
1460
+ }
1461
+
1462
+ .gradio-container {
1463
+ font-family: 'Vazirmatn', 'Tahoma', sans-serif !important;
1464
+ }
1465
+ """
1466
+
1467
+ with gr.Blocks(
1468
+ title="سیستم پیشرفته استخراج و تحلیل اسناد حقوقی فارسی",
1469
+ css=custom_css,
1470
+ theme=gr.themes.Soft()
1471
+ ) as interface:
1472
+
1473
+ # عنوان اصلی
1474
+ gr.HTML(f"""
1475
+ <div class="persian-title">
1476
+ <h1>{SVG_ICONS['document']} سیستم پیشرفته استخراج و تحلیل اسناد حقوقی فارسی</h1>
1477
+ <p>{SVG_ICONS['analyze']} مجهز به مدل‌های BERT فارسی | 📊 تحلیل هوشمند محتوا | ⚡ بهینه‌سازی شده برای Hugging Face Spaces</p>
1478
+ <p>🎯 منابع معتبر: مجلس، قوه قضاییه، وزارت دادگستری، دیوان عدالت اداری</p>
1479
+ </div>
1480
+ """)
1481
+
1482
+ with gr.Tabs():
1483
+
1484
+ # تب پردازش تک URL
1485
+ with gr.Tab(f"{SVG_ICONS['search']} پردازش تک آدرس"):
1486
+ with gr.Row():
1487
+ with gr.Column(scale=2):
1488
+ single_url = gr.Textbox(
1489
+ label=f"{SVG_ICONS['link']} آدرس سند حقوقی",
1490
+ placeholder="https://rc.majlis.ir/fa/law/show/12345",
1491
+ lines=2,
1492
+ elem_classes=["rtl"]
1493
+ )
1494
+
1495
+ with gr.Row():
1496
+ single_btn = gr.Button(
1497
+ f"{SVG_ICONS['analyze']} شروع استخراج و تحلیل",
1498
+ variant="primary",
1499
+ size="lg"
1500
+ )
1501
+ clear_single_btn = gr.Button(
1502
+ "🧹 پاک کردن",
1503
+ variant="secondary"
1504
+ )
1505
+
1506
+ with gr.Column(scale=1):
1507
+ system_status = gr.Textbox(
1508
+ label=f"{SVG_ICONS['settings']} وضعیت سیستم",
1509
+ interactive=False,
1510
+ lines=12,
1511
+ elem_classes=["rtl", "status-box"]
1512
+ )
1513
+
1514
+ with gr.Row():
1515
+ with gr.Column():
1516
+ single_status = gr.Textbox(
1517
+ label=f"{SVG_ICONS['analyze']} خلاصه نتایج",
1518
+ interactive=False,
1519
+ lines=12,
1520
+ elem_classes=["rtl"]
1521
+ )
1522
+
1523
+ with gr.Column():
1524
+ single_analysis = gr.Textbox(
1525
+ label=f"{SVG_ICONS['analyze']} تحلیل هوش مصنوعی",
1526
+ interactive=False,
1527
+ lines=12,
1528
+ elem_classes=["rtl"]
1529
+ )
1530
+
1531
+ single_content = gr.Textbox(
1532
+ label=f"{SVG_ICONS['document']} محتوای استخراج شده",
1533
+ interactive=False,
1534
+ lines=15,
1535
+ elem_classes=["rtl"]
1536
+ )
1537
+
1538
+ # تب پردازش چندتایی
1539
+ with gr.Tab(f"{SVG_ICONS['document']} پردازش دسته‌ای"):
1540
+ gr.Markdown("""
1541
+ ### 📝 راهنمای استفاده:
1542
+ - هر آدرس را در خط جداگانه‌ای قرار دهید
1543
+ - حداکثر 10 آدرس به دلیل محدودیت‌های سیستم
1544
+ - از منابع معتبر حقوقی استفاده کنید
1545
+ """, elem_classes=["rtl"])
1546
+
1547
+ multi_urls = gr.Textbox(
1548
+ label=f"{SVG_ICONS['document']} فهرست آدرس‌ها (هر آدرس در خط جداگانه)",
1549
+ placeholder="""https://rc.majlis.ir/fa/law/show/12345
1550
+ https://www.judiciary.ir/fa/news/67890
1551
+ https://www.dotic.ir/portal/law/54321""",
1552
+ lines=8,
1553
+ elem_classes=["rtl"]
1554
+ )
1555
+
1556
+ with gr.Row():
1557
+ multi_btn = gr.Button(
1558
+ f"{SVG_ICONS['analyze']} شروع پردازش دسته‌ای",
1559
+ variant="primary",
1560
+ size="lg"
1561
+ )
1562
+ clear_multi_btn = gr.Button(
1563
+ "🧹 پاک کردن",
1564
+ variant="secondary"
1565
+ )
1566
+
1567
+ with gr.Row():
1568
+ with gr.Column():
1569
+ batch_summary = gr.Textbox(
1570
+ label=f"{SVG_ICONS['analyze']} خلاصه پردازش",
1571
+ interactive=False,
1572
+ lines=10,
1573
+ elem_classes=["rtl"]
1574
+ )
1575
+
1576
+ with gr.Column():
1577
+ batch_details = gr.Textbox(
1578
+ label=f"{SVG_ICONS['document']} جزئیات نتایج",
1579
+ interactive=False,
1580
+ lines=10,
1581
+ elem_classes=["rtl"]
1582
+ )
1583
+
1584
+ # تب صادرات و مدیریت
1585
+ with gr.Tab(f"{SVG_ICONS['export']} مدیریت و صادرات"):
1586
+ with gr.Row():
1587
+ with gr.Column():
1588
+ gr.Markdown(f"### {SVG_ICONS['export']} صادرات نتایج", elem_classes=["rtl"])
1589
+
1590
+ export_btn = gr.Button(
1591
+ f"{SVG_ICONS['export']} صادرات به CSV",
1592
+ variant="primary"
1593
+ )
1594
+
1595
+ export_status = gr.Textbox(
1596
+ label="وضعیت صادرات",
1597
+ interactive=False,
1598
+ lines=3,
1599
+ elem_classes=["rtl"]
1600
+ )
1601
+
1602
+ export_file = gr.File(
1603
+ label="📁 فایل صادر شده",
1604
+ interactive=False
1605
+ )
1606
+
1607
+ with gr.Column():
1608
+ gr.Markdown(f"### {SVG_ICONS['settings']} مدیریت سیستم", elem_classes=["rtl"])
1609
+
1610
+ with gr.Row():
1611
+ refresh_btn = gr.Button(
1612
+ "🔄 بروزرسانی وضعیت",
1613
+ variant="secondary"
1614
+ )
1615
+ cleanup_btn = gr.Button(
1616
+ "🧹 پاکسازی حافظه",
1617
+ variant="secondary"
1618
+ )
1619
+
1620
+ clear_results_btn = gr.Button(
1621
+ "🗑️ پاک کردن تمام نتایج",
1622
+ variant="stop"
1623
+ )
1624
+
1625
+ management_status = gr.Textbox(
1626
+ label="وضعیت عملیات",
1627
+ interactive=False,
1628
+ lines=5,
1629
+ elem_classes=["rtl"]
1630
+ )
1631
+
1632
+ # اتصال event handlerها
1633
+
1634
+ # تک URL
1635
+ single_btn.click(
1636
+ fn=self.process_single_url,
1637
+ inputs=[single_url],
1638
+ outputs=[single_status, single_content, single_analysis],
1639
+ show_progress=True
1640
+ )
1641
+
1642
+ clear_single_btn.click(
1643
+ lambda: ("", "", "", ""),
1644
+ outputs=[single_url, single_status, single_content, single_analysis]
1645
+ )
1646
+
1647
+ # چندتایی
1648
+ multi_btn.click(
1649
+ fn=self.process_multiple_urls,
1650
+ inputs=[multi_urls],
1651
+ outputs=[batch_summary, batch_details],
1652
+ show_progress=True
1653
+ )
1654
+
1655
+ clear_multi_btn.click(
1656
+ lambda: ("", "", ""),
1657
+ outputs=[multi_urls, batch_summary, batch_details]
1658
+ )
1659
+
1660
+ # صادرات
1661
+ export_btn.click(
1662
+ fn=self.export_results,
1663
+ outputs=[export_status, export_file]
1664
+ )
1665
+
1666
+ # مدیریت
1667
+ refresh_btn.click(
1668
+ fn=self.get_system_status,
1669
+ outputs=[system_status]
1670
+ )
1671
+
1672
+ cleanup_btn.click(
1673
+ fn=MemoryManager.cleanup_memory,
1674
+ outputs=[management_status]
1675
+ ).then(
1676
+ lambda: "✅ حافظه پاکسازی شد",
1677
+ outputs=[management_status]
1678
+ )
1679
+
1680
+ clear_results_btn.click(
1681
+ fn=self.clear_results,
1682
+ outputs=[management_status]
1683
+ )
1684
+
1685
+ # بارگذاری اولیه وضعیت سیستم
1686
+ interface.load(
1687
+ fn=self.get_system_status,
1688
+ outputs=[system_status]
1689
+ )
1690
+
1691
+ return interface
1692
+
1693
+ def main():
1694
+ """تابع اصلی برای اجرای برنامه"""
1695
+ logger.info("🚀 راه‌اندازی سیستم استخراج اسناد حقوقی فارسی...")
1696
+
1697
+ try:
1698
+ # ایجاد نمونه برنامه
1699
+ app = PersianLegalScraperApp()
1700
+
1701
+ # ایجاد و راه‌اندازی رابط
1702
+ interface = app.create_interface()
1703
+
1704
+ # راه‌اندازی با پیکربندی Hugging Face Spaces
1705
+ interface.launch(
1706
+ server_name="0.0.0.0",
1707
+ server_port=7860,
1708
+ share=False,
1709
+ show_error=True,
1710
+ show_tips=True,
1711
+ enable_queue=True,
1712
+ max_threads=2
1713
+ )
1714
+
1715
+ except Exception as e:
1716
+ logger.error(f"خطا در راه‌اندازی برنامه: {e}")
1717
+ raise
1718
+
1719
+ if __name__ == "__main__":
1720
+ main()