leilaghomashchi commited on
Commit
6c28dca
·
verified ·
1 Parent(s): 7198742
Files changed (1) hide show
  1. app.py +0 -1383
app.py DELETED
@@ -1,1383 +0,0 @@
1
- import gradio as gr
2
- import re
3
- import os
4
- import requests
5
- import time
6
- import logging
7
- from packaging import version
8
-
9
- # تنظیم logging
10
- logging.basicConfig(level=logging.INFO)
11
- logger = logging.getLogger(__name__)
12
-
13
- def auto_setup_models():
14
- """راه‌اندازی خودکار مدل‌ها در صورت عدم وجود"""
15
- models_dir = "./models"
16
- required_models = {
17
- 'bert-fa-ner': 'HooshvareLab/bert-fa-zwnj-base-ner',
18
- 'bert-base-NER': 'dslim/bert-base-NER',
19
- }
20
-
21
- missing_models = []
22
- for model_name in required_models.keys():
23
- model_path = os.path.join(models_dir, model_name)
24
- if not os.path.exists(model_path) or not os.listdir(model_path):
25
- missing_models.append(model_name)
26
-
27
- if not missing_models:
28
- logger.info("✅ All models are already available")
29
- return True
30
-
31
- logger.info(f"📥 Auto-downloading missing models: {missing_models}")
32
-
33
- try:
34
- from transformers import AutoTokenizer, AutoModelForTokenClassification
35
- os.makedirs(models_dir, exist_ok=True)
36
-
37
- for model_name in missing_models:
38
- hf_repo = required_models[model_name]
39
- model_path = os.path.join(models_dir, model_name)
40
- logger.info(f"📥 Downloading {model_name} from {hf_repo}...")
41
- try:
42
- tokenizer = AutoTokenizer.from_pretrained(hf_repo)
43
- model = AutoModelForTokenClassification.from_pretrained(hf_repo)
44
- tokenizer.save_pretrained(model_path)
45
- model.save_pretrained(model_path)
46
- logger.info(f"✅ {model_name} downloaded successfully")
47
- del tokenizer, model
48
- except Exception as e:
49
- logger.error(f"❌ Failed to download {model_name}: {e}")
50
- if os.path.exists(model_path):
51
- import shutil
52
- shutil.rmtree(model_path)
53
-
54
- logger.info("🎉 Auto-setup completed!")
55
- return True
56
-
57
- except ImportError:
58
- logger.error("❌ transformers library not available for auto-download")
59
- return False
60
- except Exception as e:
61
- logger.error(f"❌ Auto-setup failed: {e}")
62
- return False
63
-
64
- # اجرای auto-setup در startup
65
- try:
66
- auto_setup_models()
67
- except Exception as e:
68
- logger.warning(f"⚠️ Auto-setup encountered an issue: {e}")
69
- logger.info("ℹ️ Continuing with manual setup...")
70
-
71
- class BilingualDataAnonymizer:
72
- def __init__(self):
73
- self.mapping_table = {}
74
- # counters به‌روزرسانی شده با دسته‌های جدید
75
- self.counters = {
76
- 'COMPANY': 0, 'PERSON': 0, 'AMOUNT': 0, 'ACCOUNT': 0,
77
- 'DATE': 0, 'STOCK_SYMBOL': 0, 'PETROCHEMICAL': 0,
78
- 'PRODUCT': 0, 'PERCENTAGE': 0, 'LOCATION': 0,
79
- 'VOLUME': 0, 'PHONE': 0, 'EMAIL': 0, 'ID_NUMBER': 0,
80
- 'FINANCIAL_TERMS': 0, 'BUSINESS_TERMS': 0, 'RATIOS': 0
81
- }
82
-
83
- self.api_key = os.getenv("OPENAI_API_KEY", "")
84
- self.models_base_path = "./models"
85
- self.models_loaded = False
86
- self.model_status = {}
87
- self.load_local_ner_models()
88
-
89
- def ensure_models_directory(self):
90
- if not os.path.exists(self.models_base_path):
91
- try:
92
- os.makedirs(self.models_base_path, exist_ok=True)
93
- logger.info(f"📁 Created models directory: {self.models_base_path}")
94
- except Exception as e:
95
- logger.error(f"❌ Failed to create models directory: {e}")
96
- return False
97
- return True
98
-
99
- def download_model_if_missing(self, local_name, hf_repo):
100
- model_path = os.path.join(self.models_base_path, local_name)
101
- if os.path.exists(model_path) and os.listdir(model_path):
102
- return True, f"Model {local_name} already exists"
103
- try:
104
- logger.info(f"📥 Auto-downloading {local_name} from {hf_repo}...")
105
- from transformers import AutoTokenizer, AutoModelForTokenClassification
106
- tokenizer = AutoTokenizer.from_pretrained(hf_repo)
107
- model = AutoModelForTokenClassification.from_pretrained(hf_repo)
108
- tokenizer.save_pretrained(model_path)
109
- model.save_pretrained(model_path)
110
- logger.info(f"✅ {local_name} auto-downloaded successfully")
111
- return True, f"Downloaded {local_name}"
112
- except Exception as e:
113
- logger.error(f"❌ Auto-download failed for {local_name}: {e}")
114
- return False, str(e)
115
-
116
- def _load_pipeline(self, task, model_path, tokenizer_path=None):
117
- """لود مدل با مدیریت صحیح پارامترهای ورژن مختلف transformers"""
118
- try:
119
- from transformers import pipeline, AutoTokenizer, AutoModelForTokenClassification, __version__ as tr_version
120
-
121
- # بررسی پشتیبانی از aggregation_strategy
122
- supports_agg = version.parse(tr_version) >= version.parse("4.11.0")
123
-
124
- # لود توکنایزر و مدل به صورت جداگانه
125
- if tokenizer_path:
126
- tokenizer = AutoTokenizer.from_pretrained(tokenizer_path, local_files_only=True)
127
- else:
128
- tokenizer = AutoTokenizer.from_pretrained(model_path, local_files_only=True)
129
-
130
- model = AutoModelForTokenClassification.from_pretrained(model_path, local_files_only=True)
131
-
132
- # ایجاد pipeline با پارامترهای مناسب
133
- pipeline_kwargs = {
134
- "model": model,
135
- "tokenizer": tokenizer,
136
- "device": -1 # استفاده از CPU
137
- }
138
-
139
- # اضافه کردن aggregation_strategy اگر پشتیبانی می‌شود
140
- if supports_agg:
141
- pipeline_kwargs["aggregation_strategy"] = "simple"
142
-
143
- return pipeline(task, **pipeline_kwargs)
144
-
145
- except Exception as e:
146
- logger.error(f"❌ Failed to load pipeline for {model_path}: {e}")
147
- return None
148
-
149
- def load_local_ner_models(self):
150
- logger.info("🔄 Loading local NER models with auto-download...")
151
- if not self.ensure_models_directory():
152
- self.models_loaded = False
153
- self.model_status['directory'] = "❌ Cannot create models directory"
154
- return
155
-
156
- try:
157
- try:
158
- import torch
159
- from transformers import AutoTokenizer, AutoModelForTokenClassification
160
- transformers_available = True
161
- logger.info("✅ Transformers library available")
162
- except ImportError as e:
163
- transformers_available = False
164
- self.model_status['transformers'] = f"❌ Transformers library not installed: {str(e)}"
165
- self.models_loaded = False
166
- return
167
-
168
- # Persian model
169
- persian_model_path = os.path.join(self.models_base_path, "bert-fa-ner")
170
- self.download_model_if_missing("bert-fa-ner", "HooshvareLab/bert-fa-zwnj-base-ner")
171
- if os.path.exists(persian_model_path) and os.listdir(persian_model_path):
172
- try:
173
- self.persian_ner = self._load_pipeline("ner", persian_model_path)
174
- if self.persian_ner:
175
- self.model_status['persian'] = f"✅ Local Persian NER: {persian_model_path}"
176
- else:
177
- self.model_status['persian'] = f"❌ Failed to load Persian model: {persian_model_path}"
178
- except Exception as e:
179
- self.persian_ner = None
180
- self.model_status['persian'] = f"❌ Persian model loading error: {str(e)[:100]}"
181
- else:
182
- self.persian_ner = None
183
- self.model_status['persian'] = f"❌ Persian model not found: {persian_model_path}"
184
-
185
- # English model
186
- english_model_path = os.path.join(self.models_base_path, "bert-base-NER")
187
- self.download_model_if_missing("bert-base-NER", "dslim/bert-base-NER")
188
- if os.path.exists(english_model_path) and os.listdir(english_model_path):
189
- try:
190
- self.english_ner = self._load_pipeline("ner", english_model_path)
191
- if self.english_ner:
192
- self.model_status['english'] = f"✅ Local English NER: {english_model_path}"
193
- else:
194
- self.model_status['english'] = f"❌ Failed to load English model: {english_model_path}"
195
- except Exception as e:
196
- self.english_ner = None
197
- self.model_status['english'] = f"❌ English model loading error: {str(e)[:100]}"
198
- else:
199
- self.english_ner = None
200
- self.model_status['english'] = f"❌ English model not found: {english_model_path}"
201
-
202
- loaded_models = sum(1 for status in self.model_status.values() if status.startswith("✅"))
203
- self.models_loaded = loaded_models > 0
204
- if loaded_models == 0:
205
- self.model_status['fallback'] = "⚠️ Using regex-only mode (no local models found)"
206
-
207
- except Exception as e:
208
- self.models_loaded = False
209
- self.model_status['critical'] = f"❌ Critical error: {str(e)[:100]}..."
210
-
211
- def detect_language(self, text):
212
- """تشخیص زبان متن"""
213
- if not text:
214
- return 'fa'
215
-
216
- persian_chars = len(re.findall(r'[\u0600-\u06FF]', text))
217
- english_chars = len(re.findall(r'[a-zA-Z]', text))
218
- total = persian_chars + english_chars
219
-
220
- if total == 0:
221
- return 'fa'
222
-
223
- if persian_chars / total > 0.6:
224
- return 'fa'
225
- elif english_chars / total > 0.6:
226
- return 'en'
227
- else:
228
- return 'mixed'
229
-
230
- def extract_entities_with_ner(self, text, lang='fa'):
231
- """استخراج entities با مدل‌های NER محلی"""
232
- entities = []
233
-
234
- if not self.models_loaded:
235
- logger.info("ℹ️ Local NER models not available - using regex only")
236
- return entities
237
-
238
- try:
239
- # مدل فارسی محلی
240
- if lang in ['fa', 'mixed'] and hasattr(self, 'persian_ner') and self.persian_ner:
241
- try:
242
- persian_results = self.persian_ner(text)
243
- for entity in persian_results:
244
- # بررسی فرمت خروجی بر اساس ورژن transformers
245
- if isinstance(entity, dict):
246
- if 'entity_group' in entity:
247
- # ورژن جدید با aggregation_strategy
248
- entities.append({
249
- 'text': entity['word'].strip(),
250
- 'label': entity['entity_group'],
251
- 'start': entity['start'],
252
- 'end': entity['end'],
253
- 'confidence': entity['score'],
254
- 'source': 'local_persian_ner'
255
- })
256
- else:
257
- # ورژن قدیمی
258
- entities.append({
259
- 'text': entity['word'].strip(),
260
- 'label': entity['entity'],
261
- 'start': entity['start'],
262
- 'end': entity['end'],
263
- 'confidence': entity['score'],
264
- 'source': 'local_persian_ner'
265
- })
266
- logger.info(f"Local Persian NER found {len(persian_results)} entities")
267
- except Exception as e:
268
- logger.error(f"Local Persian NER extraction error: {e}")
269
-
270
- # مدل انگلیسی محلی
271
- if lang in ['en', 'mixed'] and hasattr(self, 'english_ner') and self.english_ner:
272
- try:
273
- english_results = self.english_ner(text)
274
- for entity in english_results:
275
- # بررسی فرمت خروجی بر اساس ورژن transformers
276
- if isinstance(entity, dict):
277
- if 'entity_group' in entity:
278
- # ورژن جدید با aggregation_strategy
279
- entities.append({
280
- 'text': entity['word'].strip(),
281
- 'label': entity['entity_group'],
282
- 'start': entity['start'],
283
- 'end': entity['end'],
284
- 'confidence': entity['score'],
285
- 'source': 'local_english_ner'
286
- })
287
- else:
288
- # ورژن قدیمی
289
- entities.append({
290
- 'text': entity['word'].strip(),
291
- 'label': entity['entity'],
292
- 'start': entity['start'],
293
- 'end': entity['end'],
294
- 'confidence': entity['score'],
295
- 'source': 'local_english_ner'
296
- })
297
- logger.info(f"Local English NER found {len(english_results)} entities")
298
- except Exception as e:
299
- logger.error(f"Local English NER extraction error: {e}")
300
-
301
- except Exception as e:
302
- logger.error(f"Local NER extraction general error: {e}")
303
-
304
- # حذف تکراری‌ها
305
- unique_entities = []
306
- seen = set()
307
- for entity in entities:
308
- key = (entity['text'].lower(), entity['start'], entity['end'])
309
- if key not in seen:
310
- seen.add(key)
311
- unique_entities.append(entity)
312
-
313
- logger.info(f"Total unique entities found by local models: {len(unique_entities)}")
314
- return unique_entities
315
-
316
- def map_ner_to_categories(self, ner_label, source=''):
317
- """نگاشت برچسب‌های NER به دسته‌های سیستم"""
318
- mapping = {
319
- 'PER': 'PERSON', 'PERSON': 'PERSON',
320
- 'ORG': 'COMPANY', 'ORGANIZATION': 'COMPANY',
321
- 'LOC': 'LOCATION', 'LOCATION': 'LOCATION',
322
- 'MISC': 'BUSINESS_TERMS', 'MISCELLANEOUS': 'BUSINESS_TERMS',
323
- 'B-PER': 'PERSON', 'I-PER': 'PERSON',
324
- 'B-ORG': 'COMPANY', 'I-ORG': 'COMPANY',
325
- 'B-LOC': 'LOCATION', 'I-LOC': 'LOCATION',
326
- 'B-MISC': 'BUSINESS_TERMS', 'I-MISC': 'BUSINESS_TERMS',
327
- 'MONEY': 'AMOUNT', 'PERCENT': 'PERCENTAGE',
328
- 'DATE': 'DATE', 'TIME': 'DATE'
329
- }
330
- return mapping.get(ner_label.upper(), 'BUSINESS_TERMS')
331
-
332
- def anonymize_text(self, original_text, lang='fa'):
333
- """گام 1: ناشناس‌سازی متن"""
334
- try:
335
- if not original_text or not original_text.strip():
336
- return "❌ Please enter input text!" if lang == 'en' else "❌ لطفاً متن ورودی را وارد کنید!"
337
-
338
- # ریست متغیرها
339
- self.mapping_table = {}
340
- self.counters = {key: 0 for key in self.counters.keys()}
341
-
342
- anonymized = original_text
343
- found_entities = set()
344
-
345
- # تشخیص زبان
346
- detected_lang = self.detect_language(original_text)
347
- logger.info(f"Detected language: {detected_lang}")
348
-
349
- # مرحله 1: استخراج با Local NER
350
- if self.models_loaded:
351
- logger.info("🤖 Running local NER extraction...")
352
- ner_entities = self.extract_entities_with_ner(original_text, detected_lang)
353
-
354
- for entity in ner_entities:
355
- if (entity['text'] not in found_entities and
356
- len(entity['text'].strip()) > 1 and
357
- entity['confidence'] > 0.5):
358
-
359
- category = self.map_ner_to_categories(entity['label'], entity['source'])
360
-
361
- if entity['text'] not in self.mapping_table:
362
- self.counters[category] += 1
363
- code = f"{category}_{self.counters[category]:03d}_LOCAL_NER"
364
- self.mapping_table[entity['text']] = code
365
- found_entities.add(entity['text'])
366
- logger.info(f"Local NER: {entity['text']} -> {code}")
367
- else:
368
- logger.info("ℹ️ Using regex-only mode")
369
-
370
- # مرحله 2: الگوهای Regex - الگوهای جدید اضافه شده
371
- patterns = {
372
- 'STOCK_SYMBOL': [
373
- r'نماد\s+([آ-ی‌a-zA-Z0-9]+)',
374
- r'(سبهان|غدیر|شتران|شپنا|پترول|فارس|خارک|پلاسکو|جم|کرمان|مارون|اراک|رازی|شازند|کاوه|بندر|پارس|خوزستان|ماهشهر|عسلویه)(?=\s|$|،|\.|\s+—)',
375
- r'شرکت\s+([آ-ی‌a-zA-Z\s]+?)(?=\s+در|\s+که|\s+با|،|\.|\s+$|\s+را|\s+به)',
376
- r'پتروشیمی\s+([آ-ی‌a-zA-Z\s]+?)(?=\s+در|\s+که|\s+با|،|\.|\s+$|\s+توان)',
377
- r'(AAPL|GOOGL|MSFT|AMZN|TSLA|META|NVDA|SABIC)(?=\s|$|,|\.)'
378
- ],
379
- 'COMPANY': [
380
- r'شرکت(?=\s+در|\s+که|\s+با|\s+را|\s+به|\s+طی)',
381
- r'([آ-ی‌a-zA-Z\s]+)\s+شرکت',
382
- r'این\s+شرکت(?=\s|$|،|\.)',
383
- r'(بانک\s+[آ-ی‌a-zA-Z\s]+)',
384
- r'([A-Z][a-zA-Z\s]+(?:Inc|Corp|Corporation|Company|Ltd|Limited|LLC))'
385
- ],
386
- 'PERSON': [
387
- r'آقای\s+([آ-ی‌a-zA-Z]+(?:\s+[آ-ی‌a-zA-Z]+)*)',
388
- r'خانم\s+([آ-ی‌a-zA-Z]+(?:\s+[آ-ی‌a-zA-Z]+)*)',
389
- r'مهندس\s+([آ-ی‌a-zA-Z]+(?:\s+[آ-ی‌a-zA-Z]+)*)',
390
- r'دکتر\s+([آ-ی‌a-zA-Z]+(?:\s+[آ-ی‌a-zA-Z]+)*)',
391
- r'([آ-ی‌a-zA-Z]+\s+[آ-ی‌a-zA-Z]+)(?=،\s+مدیرعامل|\s+مدیرعامل|\s+رئیس)',
392
- r'مدیرعامل(?=\s|$|،|\.)',
393
- r'سرپرست(?=\s+و|\s|$|،|\.)',
394
- r'رئیس\s+هیأت‌مدیره',
395
- r'وی(?=\s+ادامه|\s+اظهار|\s+گفت|\s+اعلام|\s+همچنین)'
396
- ],
397
- 'AMOUNT': [
398
- r'\d+(?:,\d{3})*\s*(?:میلیون|میلیارد|هزار)\s*تومان',
399
- r'مبلغ\s+\d+(?:,\d{3})*\s*(?:میلیون|میلیارد|هزار)?\s*تومان',
400
- r'\d+\s*تومان(?=\s+به\s+ازای|\s+فروش|\s+،)',
401
- r'رقم\s+فعلی\s+\d+(?:,\d{3})*\s*(?:میلیون|میلیارد)\s*تومان',
402
- r'رقم\s+\d+(?:,\d{3})*\s*(?:میلیون|میلیارد)\s*تومان',
403
- r'به\s+\d+(?:,\d{3})*\s*(?:میلیون|میلیارد|هزار)\s*تومان',
404
- r'از\s+\d+(?:,\d{3})*\s*(?:میلیون|میلیارد|هزار)\s*تومان',
405
- r'برابر\s+با\s+\d+(?:,\d{3})*\s*(?:میلیون|میلیارد|هزار)\s*تومان',
406
- r'\d+(?:میلیارد|میلیون)\s*تومان(?=\s+رسیده|\s+ثبت|\s+بوده|\s+،)',
407
- r'\$\d+(?:,\d{3})*(?:\.\d+)?\s*(?:million|billion|thousand|M|B|K)?',
408
- r'\d+(?:,\d{3})*\s*ریال',
409
- r'€\d+(?:,\d{3})*(?:\.\d+)?'
410
- ],
411
- 'PERCENTAGE': [
412
- r'\d+(?:\.\d+)?\s*درصد(?:\s+افزایش|\s+رشد|\s+کاهش|\s+بالاتر|\s+پایین‌تر)?',
413
- r'\d+(?:\.\d+)?\s*%',
414
- r'معادل\s+\d+(?:\.\d+)?\s*درصد',
415
- r'حدود\s+\d+(?:\.\d+)?\s*درصد',
416
- r'با\s+\d+(?:\.\d+)?\s*درصد\s+افزایش',
417
- r'رشد\s+\d+(?:\.\d+)?\s*درصدی',
418
- r'\d+(?:\.\d+)?\s*درصدی(?=\s+همراه|\s+بوده)',
419
- r'میزان\s+رشد(?=\s+نسبت|\s+معادل)',
420
- r'افزایش\s+قابل‌توجهی',
421
- r'بهبود\s+نسبی'
422
- ],
423
-
424
- # الگوهای جدید برای اطلاعات حساس
425
- 'PHONE': [
426
- # شماره تلفن ایرانی با کد شهر
427
- r'(?:تلفن[\s:]*)?(?:شماره[\s:]*)?(?:0)?(?:[۰-۹0-9]{2,3}[-\s]?)?[۰-۹0-9]{7,8}',
428
- r'(?:تماس[\s:]*)?(?:شماره[\s:]*)?(?:با[\s]*)?(?:0)?(?:[۰-۹0-9]{2,3}[-\s]?)?[۰-۹0-9]{7,8}',
429
- r'(?:موبایل[\s:]*)?(?:شماره[\s:]*)?(?:0)?9[۰-۹0-9]{9}',
430
- # فرمت‌های مختلف شماره تلفن
431
- r'[۰-۹0-9]{3,4}[-\s][۰-۹0-9]{7,8}',
432
- r'[۰-۹0-9]{11}(?!\d)', # شماره موبایل 11 رقمی
433
- r'(?:\+98|0098)?[۰-۹0-9]{10}', # شماره با کد کشور
434
- # فرمت با خط تیره
435
- r'[۰-۹0-9]{3,4}[-\s]?[۰-۹0-9]{3,4}[-\s]?[۰-۹0-9]{3,4}'
436
- ],
437
-
438
- 'EMAIL': [
439
- # ایمیل‌های مختلف
440
- r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
441
- r'ایمیل[\s:]*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
442
- r'email[\s:]*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
443
- r'نشانی[\s]*الکترونیک[\s:]*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
444
- r'آدرس[\s]*ایمیل[\s:]*[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
445
- ],
446
-
447
- 'ACCOUNT': [
448
- # شماره حساب بانکی
449
- r'(?:شماره[\s]*)?(?:حساب[\s]*)?(?:بانکی[\s:]*)?(?:[۰-۹0-9]{1,3}[-\s]?)*[۰-۹0-9]{8,20}',
450
- r'حساب[\s]*(?:شماره[\s:]*)?(?:[۰-۹0-9]{1,3}[-\s]?)*[۰-۹0-9]{8,20}',
451
- r'شماره[\s]*حساب[\s:]*(?:[۰-۹0-9]{1,3}[-\s]?)*[۰-۹0-9]{8,20}',
452
- r'Account[\s]*(?:Number[\s:]*)?(?:[0-9]{1,3}[-\s]?)*[0-9]{8,20}',
453
- # فرمت‌های مختلف شماره حساب
454
- r'[۰-۹0-9]{3}[-\s]?[۰-۹0-9]{3}[-\s]?[۰-۹0-9]{6,12}',
455
- r'[۰-۹0-9]{2,4}[-\s]?[۰-۹0-9]{6,12}[-\s]?[۰-۹0-9]{2,4}',
456
- r'واریز[\s]*(?:سود[\s:]*)?(?:[۰-۹0-9]{1,3}[-\s]?)*[۰-۹0-9]{8,20}',
457
- r'سود[\s:]*(?:[۰-۹0-9]{1,3}[-\s]?)*[۰-۹0-9]{8,20}'
458
- ],
459
-
460
- 'ID_NUMBER': [
461
- # شماره شبا
462
- r'IR[۰-۹0-9]{24}',
463
- r'شبا[\s:]*IR[۰-۹0-9]{24}',
464
- r'IBAN[\s:]*IR[۰-۹0-9]{24}',
465
- r'شماره[\s]*شبا[\s:]*IR[۰-۹0-9]{24}',
466
- # کد ملی
467
- r'(?:کد[\s]*)?(?:ملی[\s:]*)?[۰-۹0-9]{10}',
468
- r'(?:شناسه[\s]*)?(?:ملی[\s:]*)?[۰-۹0-9]{10}',
469
- r'National[\s]*(?:ID[\s:]*)?[0-9]{10}',
470
- # شماره پاسپورت
471
- r'(?:پاسپورت[\s:]*)?[A-Z][0-9]{8}',
472
- r'(?:Passport[\s:]*)?[A-Z][0-9]{8}',
473
- # شماره کارت
474
- r'(?:کارت[\s:]*)?(?:[۰-۹0-9]{4}[-\s]?){3}[۰-۹0-9]{4}',
475
- r'(?:Card[\s:]*)?(?:[0-9]{4}[-\s]?){3}[0-9]{4}'
476
- ],
477
-
478
- 'DATE': [
479
- # تاریخ‌های مختلف
480
- r'[۰-۹0-9]{4}[/-][۰-۹0-9]{1,2}[/-][۰-۹0-9]{1,2}',
481
- r'[۰-۹0-9]{1,2}[/-][۰-۹0-9]{1,2}[/-][۰-۹0-9]{4}',
482
- # تاریخ فارسی
483
- r'(?:[۰-۹0-9]{1,2})\s*(?:فروردین|اردیبهشت|خرداد|تیر|مرداد|شهریور|مهر|آبان|آذر|دی|بهمن|اسفند)\s*(?:[۰-۹0-9]{4})',
484
- # تاریخ میلادی
485
- r'(?:[0-9]{1,2})\s*(?:January|February|March|April|May|June|July|August|September|October|November|December)\s*(?:[0-9]{4})',
486
- r'(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s*[0-9]{1,2},?\s*[0-9]{4}'
487
- ]
488
- }
489
-
490
- # پردازش patterns با اولویت‌بندی - از خاص به عام
491
- logger.info("🔍 Running prioritized regex extraction...")
492
-
493
- # پردازش به ترتیب اولویت برای جلوگیری از تداخل
494
- processed_entities = set() # برای جلوگیری از تکرار
495
-
496
- for category, pattern_list in patterns.items():
497
- for pattern in pattern_list:
498
- matches = re.finditer(pattern, original_text, re.IGNORECASE | re.MULTILINE)
499
- for match in matches:
500
- if match.groups():
501
- item = match.group(1).strip()
502
- full_match = match.group(0).strip()
503
- else:
504
- item = match.group(0).strip()
505
- full_match = item
506
-
507
- # بررسی تداخل با entities قبلی
508
- overlaps = False
509
- match_start, match_end = match.span()
510
-
511
- for proc_start, proc_end in processed_entities:
512
- # بررسی تداخل موقعیت
513
- if not (match_end <= proc_start or match_start >= proc_end):
514
- overlaps = True
515
- break
516
-
517
- if (not overlaps and
518
- full_match not in found_entities and
519
- full_match not in self.mapping_table and
520
- len(full_match) >= 2):
521
-
522
- self.counters[category] += 1
523
- code = f"{category}_{self.counters[category]:03d}_REGEX"
524
- self.mapping_table[full_match] = code
525
- found_entities.add(full_match)
526
- processed_entities.add((match_start, match_end))
527
- logger.info(f"Regex ({category}): {full_match} -> {code}")
528
-
529
- # جایگزینی در متن با ترتیب طولانی‌ترین اول
530
- sorted_items = sorted(self.mapping_table.items(), key=lambda x: len(x[0]), reverse=True)
531
- for original_item, code in sorted_items:
532
- anonymized = anonymized.replace(original_item, code)
533
-
534
- logger.info(f"✅ Anonymization completed. Found {len(self.mapping_table)} entities.")
535
- return anonymized
536
-
537
- except Exception as e:
538
- return f"❌ Error in anonymization: {str(e)}" if lang == 'en' else f"❌ خطا در ناشناس‌سازی: {str(e)}"
539
-
540
- def send_to_chatgpt(self, anonymized_text, lang='fa'):
541
- """گام 2: ارسال به ChatGPT"""
542
- try:
543
- if not anonymized_text or not anonymized_text.strip():
544
- return "❌ Anonymized text is empty!" if lang == 'en' else "❌ متن ناشناس‌شده خالی است!"
545
-
546
- if not self.api_key:
547
- return "❌ API Key not configured! Please set OPENAI_API_KEY environment variable." if lang == 'en' else "❌ کلید API تنظیم نشده است! لطفاً OPENAI_API_KEY را در متغیرهای محیطی تنظیم کنید."
548
-
549
- system_msg = "You are a professional financial analyst. The text contains anonymous codes. Answer questions accurately." if lang == 'en' else "شما یک تحلیلگر مالی حرفه‌ای هستید. متن حاوی کدهای ناشناس است. به سوالات با دقت پاسخ دهید."
550
-
551
- headers = {
552
- "Authorization": f"Bearer {self.api_key}",
553
- "Content-Type": "application/json"
554
- }
555
-
556
- data = {
557
- "model": "gpt-4o-mini",
558
- "messages": [
559
- {"role": "system", "content": system_msg},
560
- {"role": "user", "content": anonymized_text}
561
- ],
562
- "max_tokens": 2000,
563
- "temperature": 0.7
564
- }
565
-
566
- response = requests.post(
567
- "https://api.openai.com/v1/chat/completions",
568
- headers=headers,
569
- json=data,
570
- timeout=30
571
- )
572
-
573
- if response.status_code == 200:
574
- result = response.json()
575
- return result['choices'][0]['message']['content']
576
- else:
577
- error_data = response.json() if response.content else {}
578
- error_message = error_data.get('error', {}).get('message', response.text)
579
-
580
- if 'Incorrect API key' in error_message:
581
- return "❌ Invalid API key." if lang == 'en' else "❌ کلید API نامعتبر است."
582
- elif 'quota' in error_message:
583
- return "❌ API quota exceeded." if lang == 'en' else "❌ سهمیه API تمام شده است."
584
- else:
585
- return f"❌ API Error: {error_message}"
586
-
587
- except Exception as e:
588
- return f"❌ Error connecting to ChatGPT: {str(e)}" if lang == 'en' else f"❌ خطا در ارتباط با ChatGPT: {str(e)}"
589
-
590
- def deanonymize_response(self, gpt_response, lang='fa'):
591
- """گام 3: بازگردانی"""
592
- try:
593
- if not gpt_response or not gpt_response.strip():
594
- return "❌ ChatGPT response is empty!" if lang == 'en' else "❌ پاسخ ChatGPT خالی است!"
595
-
596
- if not self.mapping_table:
597
- return "❌ Mapping table is empty!" if lang == 'en' else "❌ جدول نگاشت خالی است!"
598
-
599
- final_result = gpt_response
600
- reverse_mapping = {code: original for original, code in self.mapping_table.items()}
601
-
602
- sorted_codes = sorted(reverse_mapping.items(), key=lambda x: len(x[0]), reverse=True)
603
- for code, original in sorted_codes:
604
- final_result = final_result.replace(code, original)
605
- escaped_code = code.replace('_', '\\_')
606
- final_result = final_result.replace(escaped_code, original)
607
-
608
- return final_result
609
-
610
- except Exception as e:
611
- return f"❌ Deanonymization error: {str(e)}" if lang == 'en' else f"❌ خطا در بازگردانی: {str(e)}"
612
-
613
- def get_model_status(self):
614
- """وضعیت مدل‌های محلی"""
615
- status = "🤖 **Local Model Status (Enhanced with Priority-Based Sensitive Data Detection):**\n\n"
616
-
617
- if hasattr(self, 'model_status') and self.model_status:
618
- for model_type, model_status in self.model_status.items():
619
- if model_type == 'persian':
620
- status += f"• **Persian NER**: {model_status}\n"
621
- elif model_type == 'english':
622
- status += f"• **English NER**: {model_status}\n"
623
- elif model_type == 'financial':
624
- status += f"• **Financial NER**: {model_status}\n"
625
- elif model_type == 'transformers':
626
- status += f"• **Transformers**: {model_status}\n"
627
- elif model_type == 'fallback':
628
- status += f"• **Fallback Mode**: {model_status}\n"
629
- elif model_type == 'critical':
630
- status += f"• **Critical**: {model_status}\n"
631
- elif model_type == 'directory':
632
- status += f"• **Directory**: {model_status}\n"
633
-
634
- loaded_count = sum(1 for status in getattr(self, 'model_status', {}).values()
635
- if status.startswith("✅"))
636
- status += f"\n📊 **Summary**: {loaded_count}/2 local models loaded"
637
-
638
- status += f"\n📁 **Models Path**: {self.models_base_path}"
639
- status += f"\n🔧 **Latest Features**: Priority-based detection with overlap prevention"
640
-
641
- status += f"\n\n🔍 **Enhanced Sensitive Data Detection (Priority Order):**"
642
- status += f"\n 1️⃣ **ID Numbers**: IBAN/SHEBA codes, National IDs, Passport numbers"
643
- status += f"\n 2️⃣ **Contact Info**: Email addresses with context keywords"
644
- status += f"\n 3️⃣ **Phone Numbers**: Mobile & landline with country codes"
645
- status += f"\n 4️⃣ **Bank Accounts**: Account numbers with Persian keywords"
646
- status += f"\n 5️⃣ **Financial Data**: Amounts, percentages, stock symbols"
647
- status += f"\n 6️⃣ **Corporate Data**: Company names, person names, dates"
648
-
649
- status += f"\n\n✨ **Key Improvements:**"
650
- status += f"\n 🎯 Overlap detection prevents double-matching"
651
- status += f"\n 🇮🇷 Full Persian digit support (۰-۹)"
652
- status += f"\n 🔄 Context-aware pattern matching"
653
- status += f"\n 📏 Length-based replacement order"
654
-
655
- return status
656
-
657
- # بقیه توابع مشابه قبلی...
658
- def process_all_steps(input_text, language):
659
- """پردازش خودکار تمام مراحل"""
660
- lang = 'en' if language == 'English' else 'fa'
661
-
662
- if not input_text.strip():
663
- error_msg = "❌ Please enter input text!" if lang == 'en' else "❌ لطفاً متن ورودی را وارد کنید!"
664
- return error_msg, "", "", ""
665
-
666
- try:
667
- start_time = time.time()
668
-
669
- anonymized_text = anonymizer.anonymize_text(input_text, lang)
670
- if anonymized_text.startswith("❌"):
671
- return anonymized_text, "", "", ""
672
-
673
- gpt_response = anonymizer.send_to_chatgpt(anonymized_text, lang)
674
- if gpt_response.startswith("❌"):
675
- entities_found = len(anonymizer.mapping_table)
676
- local_ner_count = sum(1 for code in anonymizer.mapping_table.values() if '_LOCAL_NER' in code)
677
- regex_count = sum(1 for code in anonymizer.mapping_table.values() if '_REGEX' in code)
678
-
679
- # آمار اطلاعات حساس
680
- sensitive_categories = ['ID_NUMBER', 'EMAIL', 'PHONE', 'ACCOUNT']
681
- sensitive_count = sum(1 for code in anonymizer.mapping_table.values()
682
- if any(cat in code for cat in sensitive_categories))
683
-
684
- method = "Priority-Based Local NER + Regex" if anonymizer.models_loaded else "Priority-Based Regex Only"
685
- success_msg = (f"✅ Anonymization completed with {method}!\n"
686
- f"🔍 Sensitive data: {sensitive_count} | 🤖 NER: {local_ner_count} | 🔎 Regex: {regex_count}\n"
687
- f"📊 Total: {entities_found} entities protected")
688
- return success_msg, anonymized_text, gpt_response, ""
689
-
690
- final_result = anonymizer.deanonymize_response(gpt_response, lang)
691
-
692
- total_time = time.time() - start_time
693
- entities_found = len(anonymizer.mapping_table)
694
- local_ner_count = sum(1 for code in anonymizer.mapping_table.values() if '_LOCAL_NER' in code)
695
- regex_count = sum(1 for code in anonymizer.mapping_table.values() if '_REGEX' in code)
696
-
697
- # آمار تفصیلی اطلاعات حساس
698
- id_count = sum(1 for code in anonymizer.mapping_table.values() if 'ID_NUMBER' in code)
699
- email_count = sum(1 for code in anonymizer.mapping_table.values() if 'EMAIL' in code)
700
- phone_count = sum(1 for code in anonymizer.mapping_table.values() if 'PHONE' in code)
701
- account_count = sum(1 for code in anonymizer.mapping_table.values() if 'ACCOUNT' in code)
702
-
703
- sensitive_details = []
704
- if id_count > 0: sensitive_details.append(f"🆔 IDs: {id_count}")
705
- if email_count > 0: sensitive_details.append(f"📧 Emails: {email_count}")
706
- if phone_count > 0: sensitive_details.append(f"📞 Phones: {phone_count}")
707
- if account_count > 0: sensitive_details.append(f"🏦 Accounts: {account_count}")
708
-
709
- method = "Priority-Based Local NER + Regex" if anonymizer.models_loaded else "Priority-Based Regex Only"
710
- success_msg = (f"🎉 Complete anonymization & restoration successful!\n"
711
- f"🔧 Method: {method}\n"
712
- f"🔍 Sensitive data: {' | '.join(sensitive_details) if sensitive_details else '0'}\n"
713
- f"📊 Total: {entities_found} entities | ⏱️ Time: {total_time:.2f}s")
714
-
715
- return success_msg, anonymized_text, gpt_response, final_result
716
-
717
- except Exception as e:
718
- error_msg = f"❌ Processing error: {str(e)}" if lang == 'en' else f"❌ خطا در پردازش: {str(e)}"
719
- return error_msg, "", "", ""
720
-
721
- def get_mapping_table(language):
722
- """نمایش جدول نگاشت"""
723
- lang = 'en' if language == 'English' else 'fa'
724
-
725
- if not anonymizer.mapping_table:
726
- return "❌ Mapping table is empty! Please process some text first." if lang == 'en' else "❌ جدول نگاشت خالی است! ابتدا متنی را پردازش کنید."
727
-
728
- result = "📋 **Priority-Based Sensitive Data Mapping Table:**\n\n" if lang == 'en' else "📋 **جدول نگاشت اطلاعات حساس با اولویت‌بندی:**\n\n"
729
-
730
- local_ner_items = {k: v for k, v in anonymizer.mapping_table.items() if '_LOCAL_NER' in v}
731
- regex_items = {k: v for k, v in anonymizer.mapping_table.items() if '_REGEX' in v}
732
-
733
- # گروه‌بندی بر اساس نوع اطلاعات حساس
734
- priority_categories = {
735
- 'ID_NUMBER': '🆔 **Identity & Financial Codes**',
736
- 'EMAIL': '📧 **Email Addresses**',
737
- 'PHONE': '📞 **Phone Numbers**',
738
- 'ACCOUNT': '🏦 **Bank Account Numbers**'
739
- }
740
-
741
- sensitive_found = False
742
- for category, title in priority_categories.items():
743
- category_items = {k: v for k, v in anonymizer.mapping_table.items() if category in v}
744
- if category_items:
745
- sensitive_found = True
746
- result += f"{title}:\n"
747
- for original, code in list(category_items.items())[:8]:
748
- result += f" • `{original}` → `{code}`\n"
749
- if len(category_items) > 8:
750
- result += f" ... و {len(category_items) - 8} مورد دیگر\n"
751
- result += "\n"
752
-
753
- if local_ner_items:
754
- result += "🤖 **Local NER Detected**:\n"
755
- for original, code in list(local_ner_items.items())[:8]:
756
- result += f" • `{original}` → `{code}`\n"
757
- if len(local_ner_items) > 8:
758
- result += f" ... و {len(local_ner_items) - 8} مورد دیگر\n"
759
- result += "\n"
760
-
761
- # سایر موارد (مالی، شرکتی و غیره)
762
- other_categories = ['AMOUNT', 'PERCENTAGE', 'COMPANY', 'PERSON', 'STOCK_SYMBOL', 'DATE']
763
- other_items = {k: v for k, v in regex_items.items()
764
- if any(cat in v for cat in other_categories)}
765
-
766
- if other_items:
767
- result += "💼 **Business & Financial Data**:\n"
768
- for original, code in list(other_items.items())[:8]:
769
- result += f" • `{original}` → `{code}`\n"
770
- if len(other_items) > 8:
771
- result += f" ... و {len(other_items) - 8} مورد دیگر\n"
772
-
773
- # آمار کلی
774
- sensitive_count = sum(len({k: v for k, v in anonymizer.mapping_table.items() if cat in v})
775
- for cat in priority_categories.keys())
776
-
777
- result += f"\n📊 **Statistics**:\n"
778
- result += f"🔍 **Sensitive Data**: {sensitive_count} items\n"
779
- result += f"🤖 **NER Detected**: {len(local_ner_items)} items\n"
780
- result += f"💼 **Business Data**: {len(other_items)} items\n"
781
- result += f"📋 **Total**: {len(anonymizer.mapping_table)} entities\n"
782
-
783
- result += f"\n✨ **Enhancement Applied**: Priority-based detection with overlap prevention\n"
784
- result += f"🎯 **Success**: All major sensitive data types detected and anonymized!"
785
-
786
- return result
787
-
788
- def clear_all():
789
- """پاک کردن همه"""
790
- anonymizer.mapping_table = {}
791
- anonymizer.counters = {key: 0 for key in anonymizer.counters.keys()}
792
- return "", "", "", "", ""
793
-
794
- def update_ui_text(language):
795
- """به‌روزرسانی متن‌های رابط کاربری"""
796
- if language == 'English':
797
- return {
798
- 'title': 'Priority-Based Bilingual Data Anonymization System',
799
- 'step1': 'Input Text & Settings',
800
- 'step2': 'Anonymized Text',
801
- 'step3': 'Raw ChatGPT Response',
802
- 'step4': 'Final Restored Response',
803
- 'input_placeholder': 'Enter your original text here...\nExample: Company reports, person names, financial amounts, phone numbers, emails, IBAN codes, bank accounts, etc.',
804
- 'process_btn': 'Process with Smart Priority Detection',
805
- 'clear_btn': 'Clear All',
806
- 'mapping_btn': 'Show Priority-Based Mapping Table',
807
- 'copy_btn': 'Copy',
808
- 'direction': 'ltr'
809
- }
810
- else:
811
- return {
812
- 'title': 'سیستم ناشناس‌سازی هوشمند با اولویت‌بندی',
813
- 'step1': 'متن ورودی و تنظیمات',
814
- 'step2': 'متن ناشناس‌شده',
815
- 'step3': 'پاسخ خام ChatGPT',
816
- 'step4': 'پاسخ نهایی بازگردانده شده',
817
- 'input_placeholder': 'متن اصلی خود را اینجا وارد کنید...\nمثال: گزارش‌های شرکت، نام اشخاص، مبالغ مالی، شماره تلفن، ایمیل، شماره شبا، حساب بانکی و غیره',
818
- 'process_btn': 'پردازش با تشخیص هوشمند اولویت‌دار',
819
- 'clear_btn': 'پاک کردن همه',
820
- 'mapping_btn': 'نمایش جدول نگاشت اولویت‌دار',
821
- 'copy_btn': 'کپی',
822
- 'direction': 'rtl'
823
- }
824
-
825
- def update_interface(language):
826
- """تغییر رابط کاربری بر اساس زبان"""
827
- ui_text = update_ui_text(language)
828
- is_english = (language == 'English')
829
-
830
- # تغییر direction برای workflow
831
- workflow_css = "workflow ltr" if is_english else "workflow rtl"
832
-
833
- return [
834
- gr.update(value=f"<h1 style='text-align: center; color: #FFD700; font-size: 3.5em; font-weight: bold; text-shadow: 3px 3px 6px rgba(0,0,0,0.5); margin: 20px 0; background: linear-gradient(45deg, #FFD700, #FFA500); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;'>📊 {ui_text['title']}</h1>"),
835
- gr.update(value=f"<h2 style='direction: {ui_text['direction']};'>📝 {ui_text['step1']}</h2>"),
836
- gr.update(placeholder=ui_text['input_placeholder'], rtl=not is_english),
837
- gr.update(value=f"🚀 {ui_text['process_btn']}"),
838
- gr.update(value=f"🗑️ {ui_text['clear_btn']}"),
839
- gr.update(rtl=not is_english),
840
- gr.update(value=f"<h2 style='direction: {ui_text['direction']};'>🎭 {ui_text['step2']}</h2>"),
841
- gr.update(rtl=not is_english),
842
- gr.update(value=f"<h2 style='direction: {ui_text['direction']};'>🤖 {ui_text['step3']}</h2>"),
843
- gr.update(rtl=not is_english),
844
- gr.update(value=f"<h2 style='direction: {ui_text['direction']};'>✅ {ui_text['step4']}</h2>"),
845
- gr.update(rtl=not is_english),
846
- gr.update(value=f"📋 {ui_text['mapping_btn']}"),
847
- gr.update(rtl=not is_english),
848
- gr.update(elem_classes=workflow_css) # اضافه شد برای تغییر direction
849
- ]
850
-
851
- # ایجاد instance
852
- anonymizer = BilingualDataAnonymizer()
853
-
854
- # CSS اصلاح شده برای ترازبندی عمودی مناسب
855
- css = """
856
- body, .gradio-container {
857
- # CSS اصلاح شده برای ترازبندی عمودی مناسب
858
- css = """
859
- body, .gradio-container {
860
- font-family: 'Segoe UI', Tahoma, Arial, sans-serif !important;
861
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
862
- min-height: 100vh !important;
863
- padding: 20px !important;
864
- }
865
-
866
- /* RTL/LTR Support */
867
- .rtl {
868
- direction: rtl !important;
869
- text-align: right !important;
870
- }
871
-
872
- .ltr {
873
- direction: ltr !important;
874
- text-align: left !important;
875
- }
876
-
877
- /* کلید اصلاح: ترازبندی عمودی دقیق گرید */
878
- .workflow {
879
- display: grid !important;
880
- grid-template-columns: 1fr 1fr 1fr 1fr !important;
881
- gap: 25px !important;
882
- padding: 30px !important;
883
- align-items: start !important; /* همه از بالا شروع شوند */
884
- align-content: start !important; /* محتوای grid هم از بالا */
885
- grid-auto-rows: auto !important; /* ارتفاع خودکار */
886
- }
887
-
888
- /* اطمینان از شروع همه آیتم‌ها از بالا */
889
- .workflow > * {
890
- align-self: start !important;
891
- vertical-align: top !important;
892
- margin-top: 0 !important; /* حذف margin اضافی */
893
- }
894
-
895
- /* اصلاح ستون‌ها در گرید - کلیدی برای ترازبندی */
896
- .workflow .gradio-column,
897
- .workflow-column {
898
- display: flex !important;
899
- flex-direction: column !important;
900
- align-items: stretch !important;
901
- justify-content: flex-start !important; /* کلیدی: از بالا شروع */
902
- height: auto !important;
903
- min-height: 0 !important;
904
- margin-top: 0 !important;
905
- padding-top: 0 !important;
906
- }
907
-
908
- /* ارتفاع یکسان و ثابت برای همه textboxها */
909
- .gradio-textbox {
910
- border-radius: 10px !important;
911
- box-shadow: 0 4px 15px rgba(0,0,0,0.1) !important;
912
- flex-grow: 1 !important;
913
- min-height: 380px !important; /* ارتفاع ثابت و یکسان برای همه */
914
- max-height: 380px !important; /* محدود کردن ارتفاع */
915
- height: 380px !important; /* ارتفاع دقیق */
916
- }
917
-
918
- /* textbox داخلی */
919
- .gradio-textbox textarea {
920
- min-height: 350px !important;
921
- max-height: 350px !important;
922
- height: 350px !important;
923
- resize: vertical !important;
924
- }
925
-
926
- /* جهت‌گیری RTL برای فارسی */
927
- .workflow.rtl {
928
- direction: rtl !important;
929
- }
930
-
931
- /* جهت‌گیری LTR برای انگلیسی */
932
- .workflow.ltr {
933
- direction: ltr !important;
934
- }
935
-
936
- /* تنظیمات تایتل و هدر - کلیدی برای ترازبندی */
937
- h1, h2, h3 {
938
- text-shadow: 2px 2px 4px rgba(0,0,0,0.3) !important;
939
- margin-top: 0 !important; /* کلیدی: حذف فاصله بالا */
940
- margin-bottom: 10px !important; /* فاصله یکسان پایین */
941
- padding-top: 0 !important;
942
- line-height: 1.2 !important; /* ارتفاع خط یکسان */
943
- }
944
-
945
- /* تایتل‌های قسمت‌ها - ارتفاع یکسان */
946
- h2 {
947
- min-height: 40px !important; /* ارتفاع ثابت برای همه تایتل‌ها */
948
- max-height: 40px !important;
949
- display: flex !important;
950
- align-items: center !important;
951
- margin-bottom: 15px !important;
952
- }
953
-
954
- /* استایل ویژه برای باکس وضعیت */
955
- .status-box {
956
- background: linear-gradient(135deg, #4CAF50, #45a049) !important;
957
- border: 3px solid #2E7D32 !important;
958
- border-radius: 15px !important;
959
- padding: 15px !important;
960
- margin: 10px 0 !important;
961
- box-shadow: 0 8px 32px rgba(76, 175, 80, 0.3) !important;
962
- animation: pulse 2s infinite !important;
963
- min-height: 120px !important; /* ارتفاع ثابت */
964
- max-height: 120px !important;
965
- }
966
-
967
- .status-box textarea {
968
- background: rgba(255, 255, 255, 0.95) !important;
969
- border: none !important;
970
- border-radius: 10px !important;
971
- font-weight: bold !important;
972
- font-size: 1.1em !important;
973
- color: #1B5E20 !important;
974
- text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.8) !important;
975
- min-height: 80px !important;
976
- max-height: 80px !important;
977
- }
978
-
979
- @keyframes pulse {
980
- 0% { box-shadow: 0 8px 32px rgba(76, 175, 80, 0.3); }
981
- 50% { box-shadow: 0 8px 40px rgba(76, 175, 80, 0.6); }
982
- 100% { box-shadow: 0 8px 32px rgba(76, 175, 80, 0.3); }
983
- }
984
-
985
- /* دکمه‌ها */
986
- .gradio-button {
987
- border-radius: 25px !important;
988
- font-weight: bold !important;
989
- transition: all 0.3s ease !important;
990
- margin: 5px 0 !important;
991
- min-height: 50px !important; /* ارتفاع ثابت برای دکمه‌ها */
992
- max-height: 50px !important;
993
- }
994
-
995
- .gradio-button:hover {
996
- transform: translateY(-2px) !important;
997
- box-shadow: 0 6px 20px rgba(0,0,0,0.2) !important;
998
- }
999
-
1000
- /* تایتل با گرادیان طلایی */
1001
- h1 {
1002
- background: linear-gradient(45deg, #FFD700, #FFA500) !important;
1003
- -webkit-background-clip: text !important;
1004
- -webkit-text-fill-color: transparent !important;
1005
- background-clip: text !important;
1006
- min-height: 80px !important; /* ارتفاع ثابت برای تایتل اصلی */
1007
- }
1008
-
1009
- /* طراحی واکنش‌گرا */
1010
- @media (max-width: 1200px) {
1011
- .workflow {
1012
- grid-template-columns: 1fr 1fr !important;
1013
- gap: 20px !important;
1014
- }
1015
- }
1016
-
1017
- @media (max-width: 768px) {
1018
- .workflow {
1019
- grid-template-columns: 1fr !important;
1020
- gap: 15px !important;
1021
- }
1022
-
1023
- .gradio-textbox {
1024
- min-height: 300px !important;
1025
- max-height: 300px !important;
1026
- height: 300px !important;
1027
- }
1028
- }
1029
-
1030
- /* تنظیمات جهت متن - بهبود یافته */
1031
- [data-testid="textbox"]:dir(rtl) {
1032
- text-align: right !important;
1033
- direction: rtl !important;
1034
- }
1035
-
1036
- [data-testid="textbox"]:dir(ltr) {
1037
- text-align: left !important;
1038
- direction: ltr !important;
1039
- }
1040
-
1041
- /* اصلاح اضافی برای تضمین ترازبندی عمودی */
1042
- .gradio-container .gradio-column {
1043
- align-self: start !important;
1044
- vertical-align: top !important;
1045
- }
1046
-
1047
- /* تنظیم ترازبندی برای Container اصلی */
1048
- .gradio-container .gradio-row {
1049
- align-items: flex-start !important;
1050
- }
1051
-
1052
- /* اطمینان از ترازبندی مناسب در همه سطوح */
1053
- * {
1054
- box-sizing: border-box !important;
1055
- }
1056
-
1057
- .gradio-container {
1058
- align-items: start !important;
1059
- justify-content: start !important;
1060
- }
1061
- """
1062
- """
1063
-
1064
- # رابط کاربری Gradio با ترازبندی اصلاح شده
1065
- with gr.Blocks(title="📊 Priority-Based Anonymization System", theme=gr.themes.Soft(), css=css) as app:
1066
-
1067
- with gr.Row():
1068
- language_selector = gr.Radio(
1069
- choices=["فارسی", "English"],
1070
- value="فارسی",
1071
- label="Language / زبان",
1072
- interactive=True
1073
- )
1074
-
1075
- with gr.Column():
1076
- # تایتل اصلی با فونت بزرگ‌تر و رنگ متمایز
1077
- title = gr.HTML("<h1 style='text-align: center; color: #FFD700; font-size: 3.5em; font-weight: bold; text-shadow: 3px 3px 6px rgba(0,0,0,0.5); margin: 20px 0; background: linear-gradient(45deg, #FFD700, #FFA500); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;'>📊 سیستم ناشناس‌سازی هوشمند با اولویت‌بندی</h1>")
1078
-
1079
- with gr.Row(elem_classes="workflow rtl") as workflow_row:
1080
- with gr.Column(elem_classes="workflow-column"):
1081
- step1_title = gr.HTML('<h2 style="direction: rtl;">📝 متن ورودی و تنظیمات</h2>')
1082
-
1083
- input_text = gr.Textbox(
1084
- lines=15, # ارتفاع یکسان برای همه
1085
- placeholder="متن اصلی خود را اینجا وارد کنید...\n✨ سیستم هوشمند اطلاعات حساس مثل شماره تلفن، ایمیل، شماره شبا، حساب بانکی را به ترتیب اولویت تشخیص می‌دهد",
1086
- label="",
1087
- rtl=True
1088
- )
1089
-
1090
- process_btn = gr.Button("🚀 پردازش با تشخیص هوشمند اولویت‌دار", variant="primary")
1091
- clear_btn = gr.Button("🗑️ پاک کردن همه", variant="stop")
1092
-
1093
- # باکس وضعیت با استایل ویژه
1094
- status = gr.Textbox(
1095
- label="وضعیت",
1096
- lines=4,
1097
- interactive=False,
1098
- rtl=True,
1099
- elem_classes=["status-box"]
1100
- )
1101
-
1102
- with gr.Column(elem_classes="workflow-column"):
1103
- step2_title = gr.HTML('<h2 style="direction: rtl;">🎭 متن ناشناس‌شده</h2>')
1104
-
1105
- anonymized_output = gr.Textbox(
1106
- lines=15, # ارتفاع یکسان
1107
- placeholder="متن ناشناس‌شده اینجا نمایش داده می‌شود...",
1108
- label="",
1109
- interactive=False,
1110
- rtl=True
1111
- )
1112
-
1113
- with gr.Column(elem_classes="workflow-column"):
1114
- step3_title = gr.HTML('<h2 style="direction: rtl;">🤖 پاسخ خام ChatGPT</h2>')
1115
-
1116
- gpt_output = gr.Textbox(
1117
- lines=15, # ارتفاع یکسان
1118
- placeholder="پاسخ خام ChatGPT اینجا نمایش داده می‌شود...",
1119
- label="",
1120
- interactive=False,
1121
- rtl=True
1122
- )
1123
-
1124
- with gr.Column(elem_classes="workflow-column"):
1125
- step4_title = gr.HTML('<h2 style="direction: rtl;">✅ پاسخ نهایی بازگردانده شده</h2>')
1126
-
1127
- final_output = gr.Textbox(
1128
- lines=15, # ارتفاع یکسان
1129
- placeholder="پاسخ نهایی اینجا نمایش داده می‌شود...",
1130
- label="",
1131
- interactive=False,
1132
- rtl=True
1133
- )
1134
-
1135
- with gr.Row():
1136
- with gr.Column():
1137
- mapping_title = gr.HTML('<h2>🗂️ جدول نگاشت اولویت‌دار</h2>')
1138
- mapping_btn = gr.Button("📋 نمایش جدول نگاشت اولویت‌دار")
1139
-
1140
- mapping_output = gr.Textbox(
1141
- lines=10,
1142
- label="جدول نگاشت اطلاعات",
1143
- interactive=False,
1144
- visible=False,
1145
- rtl=True
1146
- )
1147
-
1148
- # Event handlers
1149
- language_selector.change(
1150
- fn=update_interface,
1151
- inputs=[language_selector],
1152
- outputs=[title, step1_title, input_text, process_btn, clear_btn,
1153
- status, step2_title, anonymized_output, step3_title, gpt_output,
1154
- step4_title, final_output, mapping_btn, mapping_output, workflow_row]
1155
- )
1156
-
1157
- process_btn.click(
1158
- fn=process_all_steps,
1159
- inputs=[input_text, language_selector],
1160
- outputs=[status, anonymized_output, gpt_output, final_output]
1161
- )
1162
-
1163
- clear_btn.click(
1164
- fn=clear_all,
1165
- outputs=[input_text, anonymized_output, gpt_output, final_output, status]
1166
- )
1167
-
1168
- mapping_btn.click(
1169
- fn=get_mapping_table,
1170
- inputs=[language_selector],
1171
- outputs=[mapping_output]
1172
- )
1173
-
1174
- mapping_btn.click(
1175
- fn=lambda: gr.update(visible=True),
1176
- outputs=[mapping_output]
1177
- )
1178
-
1179
- if __name__ == "__main__":
1180
- app.launch(share=True)
1181
- body, .gradio-container {
1182
- font-family: 'Segoe UI', Tahoma, Arial, sans-serif !important;
1183
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
1184
- min-height: 100vh !important;
1185
- padding: 20px !important;
1186
- }
1187
-
1188
- /* RTL/LTR Support */
1189
- .rtl {
1190
- direction: rtl !important;
1191
- text-align: right !important;
1192
- }
1193
-
1194
- .ltr {
1195
- direction: ltr !important;
1196
- text-align: left !important;
1197
- }
1198
-
1199
- /* کلید اصلاح: ترازبندی عمودی دقیق گرید */
1200
- .workflow {
1201
- display: grid !important;
1202
- grid-template-columns: 1fr 1fr 1fr 1fr !important;
1203
- gap: 25px !important;
1204
- padding: 30px !important;
1205
- align-items: start !important; /* همه از بالا شروع شوند */
1206
- align-content: start !important; /* محتوای grid هم از بالا */
1207
- grid-auto-rows: auto !important; /* ارتفاع خودکار */
1208
- }
1209
-
1210
- /* اطمینان از شروع همه آیتم‌ها از بالا */
1211
- .workflow > * {
1212
- align-self: start !important;
1213
- vertical-align: top !important;
1214
- margin-top: 0 !important; /* حذف margin اضافی */
1215
- }
1216
-
1217
- /* اصلاح ستون‌ها در گرید - کلیدی برای ترازبندی */
1218
- .workflow .gradio-column,
1219
- .workflow-column {
1220
- display: flex !important;
1221
- flex-direction: column !important;
1222
- align-items: stretch !important;
1223
- justify-content: flex-start !important; /* کلیدی: از بالا شروع */
1224
- height: auto !important;
1225
- min-height: 0 !important;
1226
- margin-top: 0 !important;
1227
- padding-top: 0 !important;
1228
- }
1229
-
1230
- /* ارتفاع یکسان و ثابت برای همه textboxها */
1231
- .gradio-textbox {
1232
- border-radius: 10px !important;
1233
- box-shadow: 0 4px 15px rgba(0,0,0,0.1) !important;
1234
- flex-grow: 1 !important;
1235
- min-height: 380px !important; /* ارتفاع ثابت و یکسان برای همه */
1236
- max-height: 380px !important; /* محدود کردن ارتفاع */
1237
- height: 380px !important; /* ارتفاع دقیق */
1238
- }
1239
-
1240
- /* textbox داخلی */
1241
- .gradio-textbox textarea {
1242
- min-height: 350px !important;
1243
- max-height: 350px !important;
1244
- height: 350px !important;
1245
- resize: vertical !important;
1246
- }
1247
-
1248
- /* جهت‌گیری RTL برای فارسی */
1249
- .workflow.rtl {
1250
- direction: rtl !important;
1251
- }
1252
-
1253
- /* جهت‌گیری LTR برای انگلیسی */
1254
- .workflow.ltr {
1255
- direction: ltr !important;
1256
- }
1257
-
1258
- /* تنظیمات تایتل و هدر - کلیدی برای ترازبندی */
1259
- h1, h2, h3 {
1260
- text-shadow: 2px 2px 4px rgba(0,0,0,0.3) !important;
1261
- margin-top: 0 !important; /* کلیدی: حذف فاصله بالا */
1262
- margin-bottom: 10px !important; /* فاصله یکسان پایین */
1263
- padding-top: 0 !important;
1264
- line-height: 1.2 !important; /* ارتفاع خط یکسان */
1265
- }
1266
-
1267
- /* تایتل‌های قسمت‌ها - ارتفاع یکسان */
1268
- h2 {
1269
- min-height: 40px !important; /* ارتفاع ثابت برای همه تایتل‌ها */
1270
- max-height: 40px !important;
1271
- display: flex !important;
1272
- align-items: center !important;
1273
- margin-bottom: 15px !important;
1274
- }
1275
-
1276
- /* استایل ویژه برای باکس وضعیت */
1277
- .status-box {
1278
- background: linear-gradient(135deg, #4CAF50, #45a049) !important;
1279
- border: 3px solid #2E7D32 !important;
1280
- border-radius: 15px !important;
1281
- padding: 15px !important;
1282
- margin: 10px 0 !important;
1283
- box-shadow: 0 8px 32px rgba(76, 175, 80, 0.3) !important;
1284
- animation: pulse 2s infinite !important;
1285
- min-height: 120px !important; /* ارتفاع ثابت */
1286
- max-height: 120px !important;
1287
- }
1288
-
1289
- .status-box textarea {
1290
- background: rgba(255, 255, 255, 0.95) !important;
1291
- border: none !important;
1292
- border-radius: 10px !important;
1293
- font-weight: bold !important;
1294
- font-size: 1.1em !important;
1295
- color: #1B5E20 !important;
1296
- text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.8) !important;
1297
- min-height: 80px !important;
1298
- max-height: 80px !important;
1299
- }
1300
-
1301
- @keyframes pulse {
1302
- 0% { box-shadow: 0 8px 32px rgba(76, 175, 80, 0.3); }
1303
- 50% { box-shadow: 0 8px 40px rgba(76, 175, 80, 0.6); }
1304
- 100% { box-shadow: 0 8px 32px rgba(76, 175, 80, 0.3); }
1305
- }
1306
-
1307
- /* دکمه‌ها */
1308
- .gradio-button {
1309
- border-radius: 25px !important;
1310
- font-weight: bold !important;
1311
- transition: all 0.3s ease !important;
1312
- margin: 5px 0 !important;
1313
- min-height: 50px !important; /* ارتفاع ثابت برای دکمه‌ها */
1314
- max-height: 50px !important;
1315
- }
1316
-
1317
- .gradio-button:hover {
1318
- transform: translateY(-2px) !important;
1319
- box-shadow: 0 6px 20px rgba(0,0,0,0.2) !important;
1320
- }
1321
-
1322
- /* تایتل با گرادیان طلایی */
1323
- h1 {
1324
- background: linear-gradient(45deg, #FFD700, #FFA500) !important;
1325
- -webkit-background-clip: text !important;
1326
- -webkit-text-fill-color: transparent !important;
1327
- background-clip: text !important;
1328
- min-height: 80px !important; /* ارتفاع ثابت برای تایتل اصلی */
1329
- }
1330
-
1331
- /* طراحی واکنش‌گرا */
1332
- @media (max-width: 1200px) {
1333
- .workflow {
1334
- grid-template-columns: 1fr 1fr !important;
1335
- gap: 20px !important;
1336
- }
1337
- }
1338
-
1339
- @media (max-width: 768px) {
1340
- .workflow {
1341
- grid-template-columns: 1fr !important;
1342
- gap: 15px !important;
1343
- }
1344
-
1345
- .gradio-textbox {
1346
- min-height: 300px !important;
1347
- max-height: 300px !important;
1348
- height: 300px !important;
1349
- }
1350
- }
1351
-
1352
- /* تنظیمات جهت متن - بهبود یافته */
1353
- [data-testid="textbox"]:dir(rtl) {
1354
- text-align: right !important;
1355
- direction: rtl !important;
1356
- }
1357
-
1358
- [data-testid="textbox"]:dir(ltr) {
1359
- text-align: left !important;
1360
- direction: ltr !important;
1361
- }
1362
-
1363
- /* اصلاح اضافی برای تضمین ترازبندی عمودی */
1364
- .gradio-container .gradio-column {
1365
- align-self: start !important;
1366
- vertical-align: top !important;
1367
- }
1368
-
1369
- /* تنظیم ترازبندی برای Container اصلی */
1370
- .gradio-container .gradio-row {
1371
- align-items: flex-start !important;
1372
- }
1373
-
1374
- /* اطمینان از ترازبندی مناسب در همه سطوح */
1375
- * {
1376
- box-sizing: border-box !important;
1377
- }
1378
-
1379
- .gradio-container {
1380
- align-items: start !important;
1381
- justify-content: start !important;
1382
- }
1383
- """