KashefTech commited on
Commit
c2f3d69
·
verified ·
1 Parent(s): e7207ed

Upload app_fixed11.py

Browse files
Files changed (1) hide show
  1. app_fixed11.py +818 -0
app_fixed11.py ADDED
@@ -0,0 +1,818 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import re
3
+ import os
4
+ import requests
5
+ import json
6
+ import logging
7
+ from typing import Dict, List, Tuple, Optional
8
+ from llm_sender_unified import create_llm_sender
9
+
10
+ # ✅ مدل‌های موجود - به‌روزرسانی نوامبر 2024
11
+ AVAILABLE_MODELS = {
12
+ "chatgpt": [
13
+ # GPT-5 Series (جدیدترین)
14
+ "gpt-5.1", # بهترین برای کدنویسی و وظایف agentic
15
+ "gpt-5", # مدل reasoning قبلی
16
+ # GPT-4 Series
17
+ "gpt-4.1", # هوشمندترین non-reasoning
18
+ "gpt-4o", # قدرتمند
19
+ "gpt-4o-mini", # سریع و ارزان
20
+ "gpt-4-turbo", # سریع‌تر از GPT-4
21
+ ],
22
+ "grok": [
23
+ # Grok-4 Series (جدیدترین)
24
+ "grok-4-fast-reasoning", # سریع با reasoning
25
+ "grok-4-fast-non-reasoning", # سریع بدون reasoning
26
+ "grok-4-0709", # نسخه پایدار
27
+ # Grok-3 Series
28
+ "grok-3", # قدرتمند
29
+ "grok-3-mini", # سبک
30
+ # Grok-2 Series
31
+ "grok-2-vision-1212", # با قابلیت بینایی
32
+ "grok-2-1212", # نسخه پایدار
33
+ "grok-2" # نسخه قدیمی
34
+ ]
35
+ }
36
+
37
+ logging.basicConfig(level=logging.INFO)
38
+ logger = logging.getLogger(__name__)
39
+
40
+ class AnonymizerAdvanced:
41
+ """ناشناس‌ساز پیشرفته با روش‌های متعدد"""
42
+
43
+ def __init__(
44
+ self,
45
+ cerebras_key: str = None,
46
+ llm_provider: str = "chatgpt",
47
+ llm_model: str = None,
48
+ entities_to_anonymize: List[str] = None
49
+ ):
50
+ self.cerebras_key = cerebras_key or os.getenv("CEREBRAS_API_KEY")
51
+ self.llm_provider = llm_provider
52
+ self.llm_model = llm_model
53
+ self.entities_to_anonymize = entities_to_anonymize or ["person", "company", "amount", "percent"]
54
+ self.mapping_table = {}
55
+ self.reverse_mapping = {}
56
+
57
+ # ایجاد LLM sender
58
+ self._create_llm_sender()
59
+
60
+ logger.info(f"✅ Anonymizer Advanced مقداردهی شد با {llm_provider}")
61
+
62
+ def _create_llm_sender(self):
63
+ """ایجاد LLM sender مناسب"""
64
+ try:
65
+ # ✅ همیشه از Hugging Face Secrets استفاده کن
66
+ if self.llm_provider == "chatgpt":
67
+ api_key = os.getenv("OPENAI_API_KEY")
68
+ logger.info("🔑 استفاده از OPENAI_API_KEY از Secrets")
69
+ elif self.llm_provider == "grok":
70
+ api_key = os.getenv("XAI_API_KEY")
71
+ logger.info("🔑 استفاده از XAI_API_KEY از Secrets")
72
+ else:
73
+ api_key = None
74
+ logger.warning("⚠️ Provider ناشناخته")
75
+
76
+ # ایجاد sender
77
+ self.llm_sender = create_llm_sender(
78
+ provider=self.llm_provider,
79
+ api_key=api_key,
80
+ model=self.llm_model
81
+ )
82
+
83
+ logger.info(f"✅ LLM Sender ایجاد شد: {self.llm_provider} - {self.llm_sender.model}")
84
+
85
+ except Exception as e:
86
+ logger.error(f"❌ خطا در ایجاد LLM Sender: {e}")
87
+ # fallback to ChatGPT
88
+ self.llm_sender = create_llm_sender("chatgpt")
89
+
90
+ def set_llm_provider(self, provider: str, model: str = None, entities: List[str] = None):
91
+ """تغییر provider و مدل LLM و موجودیت‌های ناشناس‌سازی"""
92
+ self.llm_provider = provider
93
+ self.llm_model = model
94
+ if entities is not None:
95
+ self.entities_to_anonymize = entities
96
+ self._create_llm_sender()
97
+ logger.info(f"✅ LLM تغییر یافت به: {provider} - {model}")
98
+ logger.info(f"✅ موجودیت‌های ناشناس‌سازی: {self.entities_to_anonymize}")
99
+
100
+ def anonymize_with_cerebras(self, text: str) -> Tuple[str, Dict]:
101
+ """ناشناس‌سازی با Cerebras - بر اساس موجودیت‌های انتخابی"""
102
+ logger.info("🧠 روش Cerebras...")
103
+
104
+ if not self.cerebras_key:
105
+ logger.error("❌ Cerebras API Key موجود نیست")
106
+ raise ValueError("Cerebras API Key مورد نیاز است")
107
+
108
+ # ✅ ساخت دستورات بر اساس موجودیت‌های انتخابی
109
+ instructions = []
110
+ instruction_number = 1
111
+
112
+ if "person" in self.entities_to_anonymize:
113
+ instructions.append(f"{instruction_number}. اسامی اشخاص → person-01, person-02, ...")
114
+ instruction_number += 1
115
+
116
+ if "company" in self.entities_to_anonymize:
117
+ instructions.append(f"{instruction_number}. نام شرکت‌ها/سازمان‌ها → company-01, company-02, ...")
118
+ instruction_number += 1
119
+
120
+ if "amount" in self.entities_to_anonymize:
121
+ instructions.append(f"{instruction_number}. مقادیر پولی → amount-01, amount-02, ...")
122
+ instruction_number += 1
123
+
124
+ if "percent" in self.entities_to_anonymize:
125
+ instructions.append(f"{instruction_number}. درصدها → percent-01, percent-02, ...")
126
+ instruction_number += 1
127
+
128
+ # اگه هیچی انتخاب نشده، متن رو همون‌طور برگردون
129
+ if not instructions:
130
+ logger.warning("⚠️ هیچ موجودیتی برای ناشناس‌سازی انتخاب نشده!")
131
+ return text, {}
132
+
133
+ instructions_text = "\n".join(instructions)
134
+ instructions_text += f"\n{instruction_number}. فقط این توکن‌ها استفاده کنید"
135
+ instructions_text += f"\n{instruction_number + 1}. شماره‌های نسخه را درست حفظ کنید"
136
+ instructions_text += f"\n{instruction_number + 2}. اگر موجودیت تکرار شود از شماره قدیمی استفاده کنید"
137
+
138
+ try:
139
+ # مرحله 1: ناشناس‌سازی متن
140
+ prompt1 = f"""متن زیر را ناشناس کنید. قوانین:
141
+ {instructions_text}
142
+
143
+ متن:
144
+ {text}
145
+
146
+ خروجی: فقط متن ناشناس شده"""
147
+
148
+ response1 = requests.post(
149
+ "https://api.cerebras.ai/v1/chat/completions",
150
+ headers={
151
+ "Authorization": f"Bearer {self.cerebras_key}",
152
+ "Content-Type": "application/json"
153
+ },
154
+ json={
155
+ "model": "llama-3.3-70b",
156
+ "messages": [{"role": "user", "content": prompt1}],
157
+ "max_tokens": 4096,
158
+ "temperature": 0.1
159
+ },
160
+ timeout=60
161
+ )
162
+
163
+ if response1.status_code != 200:
164
+ logger.error(f"❌ Cerebras Error: {response1.status_code}")
165
+ raise Exception(f"Cerebras API Error: {response1.status_code}")
166
+
167
+ anonymized_text = response1.json()['choices'][0]['message']['content'].strip()
168
+ logger.info("✅ Cerebras: ناشناس‌سازی موفق")
169
+
170
+ # مرحله 2: استخراج mapping - فقط برای موجودیت‌های انتخابی
171
+ mapping_instructions = []
172
+ json_example = "{\n"
173
+
174
+ if "person" in self.entities_to_anonymize:
175
+ mapping_instructions.append('- برای person-XX: نام کامل شخص (مثلاً "علی احمدی")')
176
+ json_example += ' "person-01": "متن اصلی کامل",\n'
177
+
178
+ if "company" in self.entities_to_anonymize:
179
+ mapping_instructions.append('- برای company-XX: نام کامل شرکت/سازمان (مثلاً "شرکت پتروشیمی")')
180
+ json_example += ' "company-01": "متن اصلی کامل",\n'
181
+
182
+ if "amount" in self.entities_to_anonymize:
183
+ mapping_instructions.append('- برای amount-XX: عدد + واحد (مثلاً "80 هزار تومان" یا "50 میلیارد ریال")')
184
+ json_example += ' "amount-01": "متن اصلی کامل با واحد",\n'
185
+
186
+ if "percent" in self.entities_to_anonymize:
187
+ mapping_instructions.append('- برای percent-XX: عدد + کلمه "درصد" (مثلاً "40 درصد" نه فقط "40")')
188
+ json_example += ' "percent-01": "عدد + درصد",\n'
189
+
190
+ json_example += " ...\n}"
191
+ mapping_instructions_text = "\n".join(mapping_instructions)
192
+
193
+ prompt2 = f"""متن اصلی:
194
+ {text}
195
+
196
+ متن ناشناس شده:
197
+ {anonymized_text}
198
+
199
+ لطفاً یک جدول mapping برای همه توکن‌های ناشناس ایجاد کن.
200
+ برای هر توکن، متن اصلی کامل آن را مشخص کن.
201
+
202
+ **مهم:**
203
+ {mapping_instructions_text}
204
+
205
+ خروجی را به این فرمت JSON بده (فقط JSON، بدون توضیح اضافی):
206
+ {json_example}"""
207
+
208
+ response2 = requests.post(
209
+ "https://api.cerebras.ai/v1/chat/completions",
210
+ headers={
211
+ "Authorization": f"Bearer {self.cerebras_key}",
212
+ "Content-Type": "application/json"
213
+ },
214
+ json={
215
+ "model": "llama-3.3-70b",
216
+ "messages": [{"role": "user", "content": prompt2}],
217
+ "max_tokens": 2048,
218
+ "temperature": 0.1
219
+ },
220
+ timeout=60
221
+ )
222
+
223
+ if response2.status_code == 200:
224
+ mapping_text = response2.json()['choices'][0]['message']['content'].strip()
225
+ mapping_text = mapping_text.replace('```json', '').replace('```', '').strip()
226
+
227
+ try:
228
+ self.mapping_table = json.loads(mapping_text)
229
+ self._fix_percent_mapping()
230
+ self.reverse_mapping = {v: k for k, v in self.mapping_table.items()}
231
+ logger.info(f"✅ Mapping استخراج شد: {len(self.mapping_table)} موجودیت")
232
+ except json.JSONDecodeError:
233
+ logger.warning("⚠️ خطا در parse کردن JSON mapping - استفاده از روش fallback")
234
+ self._extract_mapping_from_text(text, anonymized_text)
235
+ else:
236
+ logger.warning("⚠️ خطا در دریافت mapping - استفاده از روش fallback")
237
+ self._extract_mapping_from_text(text, anonymized_text)
238
+
239
+ return anonymized_text, self.mapping_table
240
+
241
+ except Exception as e:
242
+ logger.error(f"❌ Cerebras Exception: {e}")
243
+ raise
244
+
245
+ def _fix_percent_mapping(self):
246
+ """اصلاح mapping برای درصدها"""
247
+ for token, value in self.mapping_table.items():
248
+ value_str = str(value).strip()
249
+
250
+ if token.startswith('percent-'):
251
+ if not re.search(r'(درصد|%|درصدی)', value_str):
252
+ self.mapping_table[token] = f"{value_str} درصد"
253
+ logger.info(f"✅ اصلاح {token}: '{value_str}' → '{value_str} درصد'")
254
+
255
+ elif token.startswith('amount-'):
256
+ if not re.search(r'(میلیارد|میلیون|هزار|تومان|ریال|دلار|یورو|تن)', value_str):
257
+ logger.warning(f"⚠️ {token}: فقط عدد '{value_str}' - واحد مشخص نیست")
258
+
259
+ def _extract_mapping_from_text(self, original: str, anonymized: str):
260
+ """استخراج mapping از متن‌های اصلی و ناشناس شده - فقط برای موجودیت‌های انتخابی"""
261
+
262
+ # ✅ استخراج فقط توکن‌های انتخابی
263
+ all_tokens = []
264
+ for entity_type in self.entities_to_anonymize:
265
+ tokens = re.findall(f'{entity_type}-\\d+', anonymized)
266
+ all_tokens.extend([(t, entity_type) for t in tokens])
267
+
268
+ all_tokens = sorted(set(all_tokens), key=lambda x: (x[1], int(x[0].split('-')[1])))
269
+
270
+ # ✅ الگوهای موجودیت - فقط برای انتخابی‌ها
271
+ patterns = {}
272
+ if "person" in self.entities_to_anonymize:
273
+ patterns['person'] = r'\b[ء-ي]+\s+[ء-ي]+(?:\s+[ء-ي]+)*\b'
274
+ if "company" in self.entities_to_anonymize:
275
+ patterns['company'] = r'(?:شرکت|بانک|سازمان|گروه|هلدینگ)\s+[ء-ي]+(?:\s+[ء-ي]+)*'
276
+ if "amount" in self.entities_to_anonymize:
277
+ patterns['amount'] = r'\d+(?:\.\d+)?\s*(?:میلیارد|میلیون|هزار|تومان|ریال|دلار|یورو|تن)'
278
+ if "percent" in self.entities_to_anonymize:
279
+ patterns['percent'] = r'\d+(?:\.\d+)?\s*(?:درصد|%|درصدی)'
280
+
281
+ original_entities = {}
282
+ for entity_type, pattern in patterns.items():
283
+ matches = list(re.finditer(pattern, original))
284
+ original_entities[entity_type] = [m.group().strip() for m in matches]
285
+
286
+ for token, entity_type in all_tokens:
287
+ if entity_type in original_entities and original_entities[entity_type]:
288
+ token_num = int(token.split('-')[1]) - 1
289
+
290
+ if token_num < len(original_entities[entity_type]):
291
+ original_text = original_entities[entity_type][token_num]
292
+ self.mapping_table[token] = original_text
293
+ self.reverse_mapping[original_text] = token
294
+ else:
295
+ original_text = original_entities[entity_type][-1]
296
+ if token not in self.mapping_table:
297
+ self.mapping_table[token] = original_text
298
+ self.reverse_mapping[original_text] = token
299
+
300
+ def analyze_with_llm(self, anonymized_text: str, analysis_prompt: str = None) -> str:
301
+ """استفاده از LLM یکپارچه"""
302
+ logger.info(f"🤖 {self.llm_provider.upper()} اجرای پرامپت...")
303
+
304
+ if not analysis_prompt or not analysis_prompt.strip():
305
+ logger.info("⚠️ پرامپت خالی - بدون تحلیل")
306
+ return "⚠️ هیچ دستور تحلیل داده نشده است"
307
+
308
+ # ✅ ساخت پیام توجه بر اساس موجودیت‌های انتخاب‌شده
309
+ tokens_instruction = []
310
+ examples = []
311
+
312
+ if "person" in self.entities_to_anonymize:
313
+ tokens_instruction.append("person-XX")
314
+ examples.append("✅ صحیح: person-01 در جلسه حضور داشت\n❌ غلط: آقای person-01 یا شخص person-01")
315
+
316
+ if "company" in self.entities_to_anonymize:
317
+ tokens_instruction.append("company-XX")
318
+ examples.append("✅ صحیح: company-01 فعالیت کرد\n❌ غلط: شرکت company-01 یا سازمان company-01")
319
+
320
+ if "amount" in self.entities_to_anonymize:
321
+ tokens_instruction.append("amount-XX")
322
+ examples.append("✅ صحیح: درآمد amount-01 میلیون ریال\n❌ غلط: مبلغ amount-01 یا رقم amount-01")
323
+
324
+ if "percent" in self.entities_to_anonymize:
325
+ tokens_instruction.append("percent-XX")
326
+ examples.append("✅ صحیح: رشد percent-01 داشت\n❌ غلط: درصد percent-01")
327
+
328
+ tokens_str = ", ".join(tokens_instruction)
329
+ examples_str = "\n".join(examples)
330
+
331
+ combined_text = f"""متن ناشناس‌سازی شده:
332
+ {anonymized_text}
333
+
334
+ دستورات:
335
+ {analysis_prompt}
336
+
337
+ ⚠️ قوانین مهم:
338
+ 1. فقط از کدهای ناشناس موجود استفاده کن: {tokens_str}
339
+ 2. هیچ کلمه‌ای قبل یا بعد از این کدها اضافه نکن
340
+ 3. کد جدید ایجاد نکن
341
+ 4. ساختار دقیق متن را حفظ کن
342
+
343
+ مثال‌های صحیح و غلط:
344
+ {examples_str}
345
+
346
+ هشدار: اگر کلمه‌ای مثل "شرکت"، "آقای"، "مبلغ" قبل از کدها بگذاری، پاسخ غلط است!"""
347
+
348
+ try:
349
+ response = self.llm_sender.send_simple(combined_text, lang='fa')
350
+
351
+ # ✅ پاکسازی کلمات اضافی (لایه امنیتی دوم)
352
+ response = self._clean_llm_response(response)
353
+
354
+ logger.info(f"✅ {self.llm_provider.upper()}: {len(response)} کاراکتر")
355
+ return response
356
+ except Exception as e:
357
+ logger.error(f"❌ {self.llm_provider.upper()} Exception: {e}")
358
+ return f"❌ خطا در ارتباط با {self.llm_provider.upper()}: {str(e)}"
359
+
360
+ def _clean_llm_response(self, text: str) -> str:
361
+ """پاکسازی کلمات اضافی که LLM ممکن است قبل از موجودیت‌ها اضافه کرده باشد"""
362
+ logger.info("🧹 پاکسازی کلمات اضافی...")
363
+
364
+ cleaned = text
365
+ changes_made = 0
366
+
367
+ # الگوهای کلمات اضافی برای هر نوع موجودیت
368
+ patterns = []
369
+
370
+ if "person" in self.entities_to_anonymize:
371
+ patterns.extend([
372
+ (r'(?:آقای|خانم|شخص)\s+(person-\d+)', r'\1'),
373
+ (r'(person-\d+)\s+(?:نامدار|محترم)', r'\1'),
374
+ ])
375
+
376
+ if "company" in self.entities_to_anonymize:
377
+ patterns.extend([
378
+ (r'(?:شرکت|سازمان|گروه|هلدینگ|بانک)\s+(company-\d+)', r'\1'),
379
+ (r'(company-\d+)\s+(?:محترم)', r'\1'),
380
+ ])
381
+
382
+ if "amount" in self.entities_to_anonymize:
383
+ patterns.extend([
384
+ (r'(?:مبلغ|رقم|عدد)\s+(amount-\d+)', r'\1'),
385
+ ])
386
+
387
+ if "percent" in self.entities_to_anonymize:
388
+ patterns.extend([
389
+ (r'(?:درصد|%)\s+(percent-\d+)', r'\1'),
390
+ ])
391
+
392
+ # اعمال الگوها
393
+ for pattern, replacement in patterns:
394
+ new_text = re.sub(pattern, replacement, cleaned)
395
+ if new_text != cleaned:
396
+ changes_made += re.subn(pattern, replacement, cleaned)[1]
397
+ cleaned = new_text
398
+
399
+ if changes_made > 0:
400
+ logger.info(f"✅ {changes_made} کلمه اضافی حذف شد")
401
+ else:
402
+ logger.info("✅ کلمه اضافی یافت نشد")
403
+
404
+ return cleaned
405
+
406
+ def restore_text(self, anonymized_text: str) -> str:
407
+ """بازگردانی متن"""
408
+ logger.info("🔄 بازگردانی متن...")
409
+
410
+ if not self.mapping_table:
411
+ logger.warning("⚠️ جدول نگاشت خالی است")
412
+ return anonymized_text
413
+
414
+ logger.info(f"📋 تعداد موجودیت‌ها در mapping: {len(self.mapping_table)}")
415
+
416
+ restored = anonymized_text
417
+ replacements_count = 0
418
+
419
+ # جایگزینی با ترتیب از طولانی‌ترین به کوتاه‌ترین برای جلوگیری از جایگزینی اشتباه
420
+ for placeholder, original in sorted(self.mapping_table.items(), key=lambda x: len(x[0]), reverse=True):
421
+ if placeholder in restored:
422
+ restored = restored.replace(placeholder, original)
423
+ replacements_count += 1
424
+ logger.info(f"✅ {placeholder} → {original[:30]}...")
425
+ else:
426
+ logger.warning(f"⚠️ {placeholder} در متن یافت نشد!")
427
+
428
+ logger.info(f"✅ بازگردانی کامل - {replacements_count}/{len(self.mapping_table)} جایگزین شد")
429
+
430
+ # اگر هیچ جایگزینی انجام نشد، احتمالاً مشکل وجود دارد
431
+ if replacements_count == 0 and len(self.mapping_table) > 0:
432
+ logger.error("❌ هیچ توکنی جایگزین نشد! متن ورودی احتمالاً متفاوت است.")
433
+
434
+ return restored
435
+
436
+ def get_mapping_table_md(self) -> str:
437
+ """تبدیل جدول نگاشت به Markdown"""
438
+ if not self.mapping_table:
439
+ return "### 📋 جدول نگاشت\n\nهیچ موجودیتی شناسایی نشد"
440
+
441
+ table = "### 📋 جدول نگاشت\n\n"
442
+ table += "| شناسه | متن اصلی |\n"
443
+ table += "|-------|----------|\n"
444
+
445
+ for token, original in sorted(self.mapping_table.items()):
446
+ table += f"| **{token}** | {original} |\n"
447
+
448
+ return table
449
+
450
+ # متغیر سراسری
451
+ anonymizer = None
452
+
453
+ def process(
454
+ input_text: str,
455
+ analysis_prompt: str,
456
+ llm_provider: str,
457
+ llm_model: str,
458
+ anonymize_all: bool,
459
+ anonymize_person: bool,
460
+ anonymize_company: bool,
461
+ anonymize_amount: bool,
462
+ anonymize_percent: bool
463
+ ):
464
+ """پردازش متن - 4 مرحله"""
465
+ global anonymizer
466
+
467
+ if not input_text.strip():
468
+ return "", "", "", ""
469
+
470
+ # ✅ ساخت لیست موجودیت‌های انتخابی
471
+ if anonymize_all:
472
+ entities = ["person", "company", "amount", "percent"]
473
+ else:
474
+ entities = []
475
+ if anonymize_person:
476
+ entities.append("person")
477
+ if anonymize_company:
478
+ entities.append("company")
479
+ if anonymize_amount:
480
+ entities.append("amount")
481
+ if anonymize_percent:
482
+ entities.append("percent")
483
+
484
+ # اگه هیچی انتخاب نشده
485
+ if not entities:
486
+ return "", "❌ لطفاً حداقل یک موجودیت برای ناشناس‌سازی انتخاب کنید", "", ""
487
+
488
+ cerebras_key = os.getenv("CEREBRAS_API_KEY")
489
+
490
+ # ایجاد یا آپدیت anonymizer
491
+ if not anonymizer:
492
+ anonymizer = AnonymizerAdvanced(
493
+ cerebras_key,
494
+ llm_provider=llm_provider,
495
+ llm_model=llm_model,
496
+ entities_to_anonymize=entities
497
+ )
498
+ else:
499
+ anonymizer.set_llm_provider(llm_provider, llm_model, entities)
500
+ anonymizer.mapping_table = {}
501
+ anonymizer.reverse_mapping = {}
502
+
503
+ try:
504
+ logger.info("=" * 70)
505
+ logger.info(f"🚀 شروع پردازش - LLM: {llm_provider} ({llm_model})")
506
+ logger.info(f"🎯 موجودیت‌های انتخابی: {', '.join(entities)}")
507
+ logger.info("=" * 70)
508
+
509
+ # مرحله 1: ناشناس‌سازی
510
+ logger.info("🔐 مرحله 1: ناشناس‌سازی...")
511
+ anonymized_text, _ = anonymizer.anonymize_with_cerebras(input_text)
512
+ logger.info(f"✅ ناشناس‌سازی: {len(anonymized_text)} کاراکتر")
513
+
514
+ # مرحله 2: LLM (فقط اگر analysis_prompt داده شده باشد)
515
+ has_analysis = analysis_prompt and analysis_prompt.strip()
516
+
517
+ if has_analysis:
518
+ logger.info(f"🤖 مرحله 2: {llm_provider.upper()}...")
519
+ llm_response = anonymizer.analyze_with_llm(anonymized_text, analysis_prompt)
520
+ logger.info(f"✅ {llm_provider.upper()}: {len(llm_response)} کاراکتر")
521
+ else:
522
+ logger.info("⚠️ مرحله 2: بدون تحلیل LLM (پرامپت خالی)")
523
+ llm_response = "⚠️ هیچ دستور تحلیل داده نشده است"
524
+
525
+ # مرحله 3: بازگردانی
526
+ logger.info("🔄 مرحله 3: بازگردانی...")
527
+
528
+ # ✅ اصلاح: اگر تحلیل انجام نشده، متن ناشناس اصلی رو restore کن
529
+ if has_analysis:
530
+ # اگر LLM تحلیل کرده، خروجی LLM رو restore کن
531
+ restored_text = anonymizer.restore_text(llm_response)
532
+ else:
533
+ # اگر تحلیل نشده، متن ناشناس اصلی رو restore کن
534
+ restored_text = anonymizer.restore_text(anonymized_text)
535
+
536
+ logger.info("✅ بازگردانی کامل")
537
+
538
+ # مرحله 4: جدول نگاشت
539
+ logger.info("📋 مرحله 4: جدول نگاشت...")
540
+ mapping_str = anonymizer.get_mapping_table_md()
541
+ logger.info(f"✅ {len(anonymizer.mapping_table)} موجودیت")
542
+
543
+ logger.info("=" * 70)
544
+ logger.info("✅ تمام مراحل کامل!")
545
+ logger.info("=" * 70)
546
+
547
+ return restored_text, llm_response, anonymized_text, mapping_str
548
+
549
+ except Exception as e:
550
+ logger.error(f"❌ خطا: {str(e)}", exc_info=True)
551
+ return "", f"❌ خطا: {str(e)}", "", ""
552
+
553
+ def clear_all():
554
+ """پاک کردن همه"""
555
+ return "", "", "", "", "", "", True, False, False, False, False
556
+
557
+ # Gradio Interface
558
+ css_rtl = """
559
+ .input-box {
560
+ direction: rtl;
561
+ text-align: right;
562
+ }
563
+ .textbox textarea {
564
+ direction: rtl;
565
+ text-align: right;
566
+ font-family: 'Tahoma', serif;
567
+ }
568
+ .thick-divider {
569
+ border-top: 2px solid #333;
570
+ margin: 10px 0;
571
+ }
572
+ .compact-group {
573
+ margin: 0;
574
+ padding: 0;
575
+ }
576
+ .compact-checkbox label {
577
+ padding: 5px 10px !important;
578
+ margin: 3px 0 !important;
579
+ font-size: 0.95em !important;
580
+ }
581
+ """
582
+
583
+ with gr.Blocks(title="سیستم ناشناس‌سازی متون", theme=gr.themes.Soft(), css=css_rtl) as app:
584
+
585
+ gr.Markdown("# 🔐 پلتفرم امن چت با مدل‌های متنوع و ناشناس‌سازی داده‌ها", elem_classes="input-box")
586
+
587
+ # ردیف اول: تنظیمات مدل و انتخاب موجودیت‌ها
588
+ with gr.Row():
589
+ # سمت راست: تنظیمات مدل
590
+ with gr.Column(scale=1):
591
+ with gr.Group():
592
+ gr.Markdown("### ⚙️ تنظیمات مدل", elem_classes="input-box")
593
+
594
+ llm_provider = gr.Dropdown(
595
+ choices=["chatgpt", "grok"],
596
+ value="chatgpt",
597
+ label="🤖 انتخاب مدل زبانی",
598
+ interactive=True
599
+ )
600
+
601
+ llm_model = gr.Dropdown(
602
+ choices=AVAILABLE_MODELS["chatgpt"],
603
+ value="gpt-4o-mini",
604
+ label="📦 انتخاب نسخه مدل",
605
+ interactive=True
606
+ )
607
+
608
+ # سمت چپ: انتخاب موجودیت‌ها
609
+ with gr.Column(scale=1):
610
+ with gr.Group():
611
+ gr.Markdown("### 🎯 انتخاب موجودیت‌ها", elem_classes="input-box")
612
+
613
+ anonymize_all = gr.Checkbox(
614
+ label="✅ همه موجودیت‌ها",
615
+ value=True,
616
+ elem_classes="input-box compact-checkbox"
617
+ )
618
+
619
+ anonymize_person = gr.Checkbox(
620
+ label="👤 اسامی اشخاص",
621
+ value=False,
622
+ elem_classes="input-box compact-checkbox"
623
+ )
624
+
625
+ anonymize_company = gr.Checkbox(
626
+ label="🏢 نام شرکت‌ها",
627
+ value=False,
628
+ elem_classes="input-box compact-checkbox"
629
+ )
630
+
631
+ anonymize_amount = gr.Checkbox(
632
+ label="💰 ارقام مالی",
633
+ value=False,
634
+ elem_classes="input-box compact-checkbox"
635
+ )
636
+
637
+ anonymize_percent = gr.Checkbox(
638
+ label="📊 درصدها",
639
+ value=False,
640
+ elem_classes="input-box compact-checkbox"
641
+ )
642
+
643
+ # خط جداکننده پررنگ
644
+ gr.Markdown("---", elem_classes="thick-divider")
645
+
646
+ # ردیف دوم: دستورات پردازش و متن ورودی
647
+ with gr.Row():
648
+ # سمت راست: دستورات پردازش
649
+ with gr.Column(scale=1):
650
+ gr.Markdown("### 📋 دستورات پردازش", elem_classes="input-box")
651
+
652
+ analysis_prompt = gr.Textbox(
653
+ lines=22,
654
+ placeholder="مثال: این متن را خلاصه کن\nیا: نکات کلیدی را استخراج کن",
655
+ label="📋 دستورات LLM (اختیاری)",
656
+ elem_classes="textbox"
657
+ )
658
+
659
+ # سمت چپ: متن ورودی
660
+ with gr.Column(scale=1):
661
+ gr.Markdown("### 📝 متن ورودی", elem_classes="input-box")
662
+
663
+ input_text = gr.Textbox(
664
+ lines=22,
665
+ placeholder="متن مالی/خبری را وارد کنید...",
666
+ label="",
667
+ elem_classes="textbox"
668
+ )
669
+
670
+ # دکمه‌های پردازش و پاک کردن
671
+ with gr.Row():
672
+ process_btn = gr.Button(
673
+ "▶️ پردازش",
674
+ variant="primary",
675
+ size="lg",
676
+ scale=2
677
+ )
678
+
679
+ clear_btn = gr.Button(
680
+ "🗑️ پاک کردن",
681
+ variant="stop",
682
+ size="lg",
683
+ scale=1
684
+ )
685
+
686
+ # نتایج
687
+ gr.Markdown("## 📊 نتایج پردازش", elem_classes="input-box")
688
+
689
+ with gr.Row():
690
+ with gr.Column(scale=1):
691
+ restored_text = gr.Textbox(
692
+ lines=12,
693
+ label="✅ متن بازگردانی شده",
694
+ interactive=False,
695
+ elem_classes="textbox"
696
+ )
697
+
698
+ with gr.Column(scale=1):
699
+ llm_analysis = gr.Textbox(
700
+ lines=12,
701
+ label="🤖 تحلیل LLM",
702
+ interactive=False,
703
+ elem_classes="textbox"
704
+ )
705
+
706
+ with gr.Column(scale=1):
707
+ anonymized_text = gr.Textbox(
708
+ lines=12,
709
+ label="🔒 متن ناشناس‌شده",
710
+ interactive=False,
711
+ elem_classes="textbox"
712
+ )
713
+
714
+ mapping_table = gr.Markdown(
715
+ value="### 📋 جدول نگاشت\n\nهنوز پردازشی انجام نشده",
716
+ label="📋 جدول نگاشت",
717
+ elem_classes="input-box"
718
+ )
719
+
720
+
721
+ # Event Handler برای تغییر provider
722
+ def handle_provider_change(provider):
723
+ models = AVAILABLE_MODELS.get(provider, [])
724
+ default_model = models[0] if models else None
725
+ return gr.update(choices=models, value=default_model)
726
+
727
+ llm_provider.change(
728
+ fn=handle_provider_change,
729
+ inputs=[llm_provider],
730
+ outputs=[llm_model]
731
+ )
732
+
733
+ def handle_select_all(select_all):
734
+ if select_all:
735
+ return (
736
+ gr.update(value=False, interactive=False),
737
+ gr.update(value=False, interactive=False),
738
+ gr.update(value=False, interactive=False),
739
+ gr.update(value=False, interactive=False)
740
+ )
741
+ else:
742
+ return (
743
+ gr.update(value=False, interactive=True),
744
+ gr.update(value=False, interactive=True),
745
+ gr.update(value=False, interactive=True),
746
+ gr.update(value=False, interactive=True)
747
+ )
748
+
749
+ anonymize_all.change(
750
+ fn=handle_select_all,
751
+ inputs=[anonymize_all],
752
+ outputs=[anonymize_person, anonymize_company, anonymize_amount, anonymize_percent]
753
+ )
754
+
755
+ # پردازش
756
+ process_btn.click(
757
+ fn=process,
758
+ inputs=[
759
+ input_text,
760
+ analysis_prompt,
761
+ llm_provider,
762
+ llm_model,
763
+ anonymize_all,
764
+ anonymize_person,
765
+ anonymize_company,
766
+ anonymize_amount,
767
+ anonymize_percent
768
+ ],
769
+ outputs=[restored_text, llm_analysis, anonymized_text, mapping_table]
770
+ )
771
+
772
+ # پاک کردن
773
+ clear_btn.click(
774
+ fn=clear_all,
775
+ outputs=[
776
+ input_text,
777
+ analysis_prompt,
778
+ restored_text,
779
+ llm_analysis,
780
+ anonymized_text,
781
+ mapping_table,
782
+ anonymize_all,
783
+ anonymize_person,
784
+ anonymize_company,
785
+ anonymize_amount,
786
+ anonymize_percent
787
+ ]
788
+ )
789
+
790
+ if __name__ == "__main__":
791
+ print("=" * 70)
792
+ print("🚀 سیستم ناشناس‌سازی متون در حال راه‌اندازی...")
793
+ print("=" * 70)
794
+ print("\n📋 نحوه استفاده:\n")
795
+ print("1. API Keyها را در Hugging Face Secrets تنظیم کنید:")
796
+ print(" - CEREBRAS_API_KEY (ضروری برای ناشناس‌سازی)")
797
+ print(" - OPENAI_API_KEY (برای ChatGPT)")
798
+ print(" - XAI_API_KEY (برای Grok)")
799
+ print("2. http://localhost:7860 را باز کنید")
800
+ print("3. مدل زبانی (ChatGPT/Grok) و نسخه مدل را انتخاب کنید")
801
+ print("4. موجودیت‌های مورد نظر برای ناشناس‌سازی را انتخاب کنید")
802
+ print("5. متن و دستورات پردازش را وارد کنید")
803
+ print("6. 'پردازش' را کلیک کنید\n")
804
+ print("🔐 تمام API Keyها از Hugging Face Secrets خوانده می‌شوند")
805
+ print("📦 مدل‌های پشتیبانی شده:")
806
+ print(" • ChatGPT GPT-5: gpt-5.1, gpt-5")
807
+ print(" • ChatGPT GPT-4: gpt-4.1, gpt-4o, gpt-4o-mini, gpt-4-turbo")
808
+ print(" • Grok-4: grok-4-fast-reasoning, grok-4-fast-non-reasoning, grok-4-0709")
809
+ print(" • Grok-3: grok-3, grok-3-mini")
810
+ print(" • Grok-2: grok-2-vision-1212, grok-2-1212, grok-2")
811
+ print("=" * 70 + "\n")
812
+
813
+ app.launch(
814
+ server_name="0.0.0.0",
815
+ server_port=7860,
816
+ share=False,
817
+ show_error=True
818
+ )