leilaghomashchi commited on
Commit
bedec4c
·
verified ·
1 Parent(s): 1a636ef

Upload complete_business_focused_app.py

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