KashefTech commited on
Commit
8e6c9f1
·
verified ·
1 Parent(s): b5c1f14

Upload app_fixed_v6_CEREBRAS_FIX.py

Browse files
Files changed (1) hide show
  1. app_fixed_v6_CEREBRAS_FIX.py +1141 -0
app_fixed_v6_CEREBRAS_FIX.py ADDED
@@ -0,0 +1,1141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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}. اعداد و ارقام و مبالغ (مثل: 50 میلیارد، 100 هزار، 25.5 میلیون، ۳۰۰ دستگاه) → 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
+ # ✅ ساخت مثال برای amount (اگر انتخاب شده)
141
+ example_text = ""
142
+ if "amount" in self.entities_to_anonymize:
143
+ example_text = """
144
+ مثال:
145
+ متن اصلی: "فروش 50 میلیارد ریال در سال گذشته بود."
146
+ متن ناشناس: "فروش amount-01 در سال گذشته بود."
147
+ """
148
+
149
+ prompt1 = f"""متن زیر را ناشناس کنید. قوانین:
150
+ {instructions_text}
151
+ {example_text}
152
+ متن:
153
+ {text}
154
+
155
+ خروجی: فقط متن ناشناس شده (بدون توضیح اضافی)"""
156
+
157
+ response1 = requests.post(
158
+ "https://api.cerebras.ai/v1/chat/completions",
159
+ headers={
160
+ "Authorization": f"Bearer {self.cerebras_key}",
161
+ "Content-Type": "application/json"
162
+ },
163
+ json={
164
+ "model": "llama-3.3-70b",
165
+ "messages": [{"role": "user", "content": prompt1}],
166
+ "max_tokens": 4096,
167
+ "temperature": 0.1
168
+ },
169
+ timeout=60
170
+ )
171
+
172
+ if response1.status_code != 200:
173
+ logger.error(f"❌ Cerebras Error: {response1.status_code}")
174
+ raise Exception(f"Cerebras API Error: {response1.status_code}")
175
+
176
+ anonymized_text = response1.json()['choices'][0]['message']['content'].strip()
177
+ logger.info("✅ Cerebras: ناشناس‌سازی موفق")
178
+
179
+ # مرحله 2: استخراج mapping - فقط برای موجودیت‌های انتخابی
180
+ mapping_instructions = []
181
+ json_example = "{\n"
182
+
183
+ if "person" in self.entities_to_anonymize:
184
+ mapping_instructions.append('- برای person-XX: نام کامل شخص (مثلاً "علی احمدی")')
185
+ json_example += ' "person-01": "متن اصلی کامل",\n'
186
+
187
+ if "company" in self.entities_to_anonymize:
188
+ mapping_instructions.append('- برای company-XX: نام کامل شرکت/سازمان (مثلاً "شرکت پتروشیمی")')
189
+ json_example += ' "company-01": "متن اصلی کامل",\n'
190
+
191
+ if "amount" in self.entities_to_anonymize:
192
+ mapping_instructions.append('- برای amount-XX: عدد + واحد (مثلاً "80 هزار تومان" یا "50 میلیارد ریال")')
193
+ json_example += ' "amount-01": "متن اصلی کامل با واحد",\n'
194
+
195
+ if "percent" in self.entities_to_anonymize:
196
+ mapping_instructions.append('- برای percent-XX: عدد + کلمه "درصد" (مثلاً "40 درصد" نه فقط "40")')
197
+ json_example += ' "percent-01": "عدد + درصد",\n'
198
+
199
+ json_example += " ...\n}"
200
+ mapping_instructions_text = "\n".join(mapping_instructions)
201
+
202
+ prompt2 = f"""متن اصلی:
203
+ {text}
204
+
205
+ متن ناشناس شده:
206
+ {anonymized_text}
207
+
208
+ لطفاً یک جدول mapping برای همه توکن‌های ناشناس ایجاد کن.
209
+ برای هر توکن، متن اصلی کامل آن را مشخص کن.
210
+
211
+ **مهم:**
212
+ {mapping_instructions_text}
213
+
214
+ خروجی را به این فرمت JSON بده (فقط JSON، بدون توضیح اضافی):
215
+ {json_example}"""
216
+
217
+ response2 = requests.post(
218
+ "https://api.cerebras.ai/v1/chat/completions",
219
+ headers={
220
+ "Authorization": f"Bearer {self.cerebras_key}",
221
+ "Content-Type": "application/json"
222
+ },
223
+ json={
224
+ "model": "llama-3.3-70b",
225
+ "messages": [{"role": "user", "content": prompt2}],
226
+ "max_tokens": 2048,
227
+ "temperature": 0.1
228
+ },
229
+ timeout=60
230
+ )
231
+
232
+ if response2.status_code == 200:
233
+ mapping_text = response2.json()['choices'][0]['message']['content'].strip()
234
+ mapping_text = mapping_text.replace('```json', '').replace('```', '').strip()
235
+
236
+ try:
237
+ self.mapping_table = json.loads(mapping_text)
238
+ self._fix_percent_mapping()
239
+ self.reverse_mapping = {v: k for k, v in self.mapping_table.items()}
240
+ logger.info(f"✅ Mapping استخراج شد: {len(self.mapping_table)} موجودیت")
241
+ except json.JSONDecodeError:
242
+ logger.warning("⚠️ خطا در parse کردن JSON mapping - استفاده از روش fallback")
243
+ self._extract_mapping_from_text(text, anonymized_text)
244
+ else:
245
+ logger.warning("⚠️ خطا در دریافت mapping - استفاده از روش fallback")
246
+ self._extract_mapping_from_text(text, anonymized_text)
247
+
248
+ return anonymized_text, self.mapping_table
249
+
250
+ except Exception as e:
251
+ logger.error(f"❌ Cerebras Exception: {e}")
252
+ raise
253
+
254
+ def _fix_percent_mapping(self):
255
+ """اصلاح mapping برای درصدها"""
256
+ for token, value in self.mapping_table.items():
257
+ value_str = str(value).strip()
258
+
259
+ if token.startswith('percent-'):
260
+ if not re.search(r'(درصد|%|درصدی)', value_str):
261
+ self.mapping_table[token] = f"{value_str} درصد"
262
+ logger.info(f"✅ اصلاح {token}: '{value_str}' → '{value_str} درصد'")
263
+
264
+ elif token.startswith('amount-'):
265
+ if not re.search(r'(میلیارد|میلیون|هزار|تومان|ریال|دلار|یورو|تن)', value_str):
266
+ logger.warning(f"⚠️ {token}: فقط عدد '{value_str}' - واحد مشخص نیست")
267
+
268
+ def _extract_mapping_from_text(self, original: str, anonymized: str):
269
+ """استخراج mapping از متن‌های اصلی و ناشناس شده - فقط برای موجودیت‌های انتخابی"""
270
+
271
+ # ✅ استخراج فقط توکن‌های انتخابی
272
+ all_tokens = []
273
+ for entity_type in self.entities_to_anonymize:
274
+ tokens = re.findall(f'{entity_type}-\\d+', anonymized)
275
+ all_tokens.extend([(t, entity_type) for t in tokens])
276
+
277
+ all_tokens = sorted(set(all_tokens), key=lambda x: (x[1], int(x[0].split('-')[1])))
278
+
279
+ # ✅ الگوهای موجودیت - فقط برای انتخابی‌ها
280
+ patterns = {}
281
+ if "person" in self.entities_to_anonymize:
282
+ patterns['person'] = r'\b[ء-ي]+\s+[ء-ي]+(?:\s+[ء-ي]+)*\b'
283
+ if "company" in self.entities_to_anonymize:
284
+ patterns['company'] = r'(?:شرکت|بانک|سازمان|گروه|هلدینگ)\s+[ء-ي]+(?:\s+[ء-ي]+)*'
285
+ if "amount" in self.entities_to_anonymize:
286
+ patterns['amount'] = r'\d+(?:\.\d+)?\s*(?:میلیارد|میلیون|هزار|تومان|ریال|دلار|یورو|تن)'
287
+ if "percent" in self.entities_to_anonymize:
288
+ patterns['percent'] = r'\d+(?:\.\d+)?\s*(?:درصد|%|درصدی)'
289
+
290
+ original_entities = {}
291
+ for entity_type, pattern in patterns.items():
292
+ matches = list(re.finditer(pattern, original))
293
+ original_entities[entity_type] = [m.group().strip() for m in matches]
294
+
295
+ for token, entity_type in all_tokens:
296
+ if entity_type in original_entities and original_entities[entity_type]:
297
+ token_num = int(token.split('-')[1]) - 1
298
+
299
+ if token_num < len(original_entities[entity_type]):
300
+ original_text = original_entities[entity_type][token_num]
301
+ self.mapping_table[token] = original_text
302
+ self.reverse_mapping[original_text] = token
303
+ else:
304
+ original_text = original_entities[entity_type][-1]
305
+ if token not in self.mapping_table:
306
+ self.mapping_table[token] = original_text
307
+ self.reverse_mapping[original_text] = token
308
+
309
+ def analyze_with_llm(self, anonymized_text: str, analysis_prompt: str = None) -> str:
310
+ """استفاده از LLM یکپارچه"""
311
+ logger.info(f"🤖 {self.llm_provider.upper()} اجرای پرامپت...")
312
+
313
+ if not analysis_prompt or not analysis_prompt.strip():
314
+ logger.info("⚠️ پرامپت خالی - بدون تحلیل")
315
+ return "⚠️ هیچ دستور تحلیل داده نشده است"
316
+
317
+ # ✅ بررسی اینکه آیا مدل GPT-4 است
318
+ is_gpt4 = self.llm_model and any(x in self.llm_model.lower() for x in ['gpt-4', 'gpt4'])
319
+
320
+ if is_gpt4:
321
+ # ✅ پرامپت ویژه GPT-4 با مثال‌های واقعی
322
+ logger.info("🎯 استفاده از پرامپت ویژه GPT-4")
323
+ return self._analyze_with_gpt4_prompt(anonymized_text, analysis_prompt)
324
+ else:
325
+ # پرامپت عادی برای GPT-5 و Grok
326
+ return self._analyze_with_standard_prompt(anonymized_text, analysis_prompt)
327
+
328
+ def _analyze_with_gpt4_prompt(self, anonymized_text: str, analysis_prompt: str) -> str:
329
+ """پرامپت ویژه GPT-4 با few-shot examples"""
330
+
331
+ # ✅ مثال‌های واقعی Few-Shot
332
+ few_shot_examples = """
333
+ EXAMPLE 1 - CORRECT:
334
+ Input: "company-01 فروش amount-01 داشت"
335
+ Your output should be EXACTLY: "company-01 فروش amount-01 داشت"
336
+ NOT: "company-01 فروش مبلغ amount-01 داشت"
337
+
338
+ EXAMPLE 2 - CORRECT:
339
+ Input: "amount-02 به amount-03 رسید"
340
+ Your output should be EXACTLY: "amount-02 به amount-03 رسید"
341
+ NOT: "مبلغ amount-02 به amount-03 رسید"
342
+
343
+ EXAMPLE 3 - CORRECT:
344
+ Input: "company-01 سود percent-01 داشت"
345
+ Your output should be EXACTLY: "company-01 سود percent-01 داشت"
346
+ NOT: "شرکت company-01 سود درصد percent-01 داشت"
347
+ """
348
+
349
+ # لیست توکن‌های انتخابی
350
+ tokens_list = []
351
+ if "person" in self.entities_to_anonymize:
352
+ tokens_list.append("person-XX")
353
+ if "company" in self.entities_to_anonymize:
354
+ tokens_list.append("company-XX")
355
+ if "amount" in self.entities_to_anonymize:
356
+ tokens_list.append("amount-XX")
357
+ if "percent" in self.entities_to_anonymize:
358
+ tokens_list.append("percent-XX")
359
+
360
+ tokens_str = ", ".join(tokens_list)
361
+
362
+ # ✅ پرامپت انگلیسی برای GPT-4 (بهتر کار می‌کند)
363
+ combined_text = f"""You are processing anonymized Persian/Farsi text containing placeholder tokens.
364
+
365
+ ANONYMIZED TEXT:
366
+ {anonymized_text}
367
+
368
+ USER REQUEST:
369
+ {analysis_prompt}
370
+
371
+ CRITICAL RULES:
372
+ 1. Use ONLY these exact tokens: {tokens_str}
373
+ 2. NEVER add words before/after tokens
374
+ 3. Keep the EXACT format: amount-01 (not "مبلغ amount-01" or "amount- 01")
375
+ 4. Do NOT create new tokens
376
+ 5. Preserve the exact structure
377
+
378
+ {few_shot_examples}
379
+
380
+ FORBIDDEN PATTERNS - NEVER USE:
381
+ ❌ "مبلغ amount-01" → ✅ Use: "amount-01"
382
+ ❌ "شرکت company-01" → ✅ Use: "company-01"
383
+ ❌ "فروش به amount-02" → ✅ Use: "فروش amount-02"
384
+ ❌ "درصد percent-01" → ✅ Use: "percent-01"
385
+ ❌ "amount- 01" (space) → ✅ Use: "amount-01"
386
+
387
+ Now process the text following these rules EXACTLY."""
388
+
389
+ try:
390
+ # ✅ temperature خیلی پایین برای GPT-4
391
+ logger.info(f"🌡️ Temperature: 0.05 (GPT-4 ویژه)")
392
+
393
+ response = self.llm_sender.send(
394
+ combined_text,
395
+ lang='en', # انگلیسی برای GPT-4
396
+ temperature=0.05, # خیلی خیلی پایین
397
+ max_tokens=2000
398
+ )
399
+
400
+ # ✅ دیباگ: نمایش خروجی خام LLM
401
+ logger.info("=" * 60)
402
+ logger.info("🔍 DEBUG - خروجی خام GPT-4:")
403
+ logger.info(response[:500] + "..." if len(response) > 500 else response)
404
+ logger.info("=" * 60)
405
+
406
+ # ✅ پاکسازی قوی‌تر
407
+ cleaned_response = self._clean_llm_response(response)
408
+
409
+ # ✅ دیباگ: نمایش خروجی بعد از clean
410
+ logger.info("=" * 60)
411
+ logger.info("🧹 DEBUG - خروجی بعد از clean:")
412
+ logger.info(cleaned_response[:500] + "..." if len(cleaned_response) > 500 else cleaned_response)
413
+ logger.info("=" * 60)
414
+
415
+ logger.info(f"✅ GPT-4: {len(cleaned_response)} کاراکتر")
416
+ return cleaned_response
417
+
418
+ except Exception as e:
419
+ logger.error(f"❌ GPT-4 Exception: {e}")
420
+ return f"❌ خطا در ارتباط با GPT-4: {str(e)}"
421
+
422
+ def _analyze_with_standard_prompt(self, anonymized_text: str, analysis_prompt: str) -> str:
423
+ """پرامپت استاندارد برای GPT-5 و Grok"""
424
+
425
+ tokens_instruction = []
426
+ examples = []
427
+
428
+ if "person" in self.entities_to_anonymize:
429
+ tokens_instruction.append("person-XX")
430
+ examples.append("✅ صحیح: person-01 در جلسه حضور داشت\n❌ غلط: آقای person-01")
431
+
432
+ if "company" in self.entities_to_anonymize:
433
+ tokens_instruction.append("company-XX")
434
+ examples.append("✅ صحیح: company-01 فعالیت کرد\n❌ غلط: شرکت company-01")
435
+
436
+ if "amount" in self.entities_to_anonymize:
437
+ tokens_instruction.append("amount-XX")
438
+ examples.append("✅ صحیح: فروش amount-01 بود\n❌ غلط: فروش مبلغ amount-01")
439
+
440
+ if "percent" in self.entities_to_anonymize:
441
+ tokens_instruction.append("percent-XX")
442
+ examples.append("✅ صحیح: رشد percent-01 داشت\n❌ غلط: رشد درصد percent-01")
443
+
444
+ tokens_str = ", ".join(tokens_instruction)
445
+ examples_str = "\n".join(examples)
446
+
447
+ combined_text = f"""متن ناشناس‌سازی شده:
448
+ {anonymized_text}
449
+
450
+ دستورات:
451
+ {analysis_prompt}
452
+
453
+ ⚠️ قوانین مهم:
454
+ 1. فقط از کدهای ناشناس موجود استفاده کن: {tokens_str}
455
+ 2. هیچ کلمه‌ای قبل یا بعد از این کدها اضافه نکن
456
+ 3. کد جدید ایجاد نکن
457
+ 4. ساختار دقیق متن را حفظ کن
458
+
459
+ مثال‌های صحیح و غلط:
460
+ {examples_str}"""
461
+
462
+ try:
463
+ temp_to_use = 0.2
464
+ logger.info(f"🌡️ Temperature: {temp_to_use}")
465
+
466
+ response = self.llm_sender.send(
467
+ combined_text,
468
+ lang='fa',
469
+ temperature=temp_to_use,
470
+ max_tokens=2000
471
+ )
472
+
473
+ response = self._clean_llm_response(response)
474
+
475
+ logger.info(f"✅ {self.llm_provider.upper()}: {len(response)} کاراکتر")
476
+ return response
477
+
478
+ except Exception as e:
479
+ logger.error(f"❌ {self.llm_provider.upper()} Exception: {e}")
480
+ return f"❌ خطا در ارتباط با {self.llm_provider.upper()}: {str(e)}"
481
+
482
+ def _clean_llm_response(self, text: str) -> str:
483
+ """پاکسازی کلمات اضافی که LLM ممکن است قبل از موجودیت‌ها اضافه کرده باشد"""
484
+ logger.info("🧹 پاکسازی کلمات اضافی...")
485
+
486
+ cleaned = text
487
+ changes_made = 0
488
+
489
+ # الگوهای کلمات اضافی برای هر نوع موجودیت
490
+ patterns = []
491
+
492
+ if "person" in self.entities_to_anonymize:
493
+ patterns.extend([
494
+ (r'(?:آقای|خانم|شخص|فرد)\s+(person-\d+)', r'\1'),
495
+ (r'(person-\d+)\s+(?:نامدار|محترم|عزیز)', r'\1'),
496
+ ])
497
+
498
+ if "company" in self.entities_to_anonymize:
499
+ patterns.extend([
500
+ (r'(?:شرکت|سازمان|گروه|هلدینگ|بانک|موسسه)\s+(company-\d+)', r'\1'),
501
+ (r'(company-\d+)\s+(?:محترم)', r'\1'),
502
+ ])
503
+
504
+ if "amount" in self.entities_to_anonymize:
505
+ patterns.extend([
506
+ # ✅ الگوهای کامل برای amount - تمام حالات ممکن
507
+ # حالت 1: کلمات قبل از amount
508
+ (r'(?:مبلغ|رقم|عدد|قیمت|ارزش|مقدار)\s+(amount-\d+)', r'\1'),
509
+ (r'(?:فروش|درآمد|سود|زیان|هزینه|خرج)\s+(amount-\d+)', r'\1'),
510
+ (r'(?:دارایی|بدهی|سرمایه|پول|وام)\s+(amount-\d+)', r'\1'),
511
+
512
+ # حالت 2: حروف اضافه قبل از amount
513
+ (r'\bبه\s+(amount-\d+)', r'\1'),
514
+ (r'\bبا\s+(amount-\d+)', r'\1'),
515
+ (r'\bاز\s+(amount-\d+)', r'\1'),
516
+ (r'\bتا\s+(amount-\d+)', r'\1'),
517
+ (r'\bدر\s+(amount-\d+)', r'\1'),
518
+ (r'\bبرای\s+(amount-\d+)', r'\1'),
519
+
520
+ # حالت 3: واحدها بعد از amount (اگر نباید باشند)
521
+ (r'(amount-\d+)\s+(?:ریال|تومان|دلار|یورو)', r'\1'),
522
+ (r'(amount-\d+)\s+(?:میلیون|میلیارد|هزار|تریلیون)', r'\1'),
523
+
524
+ # حالت 4: ترکیبات
525
+ (r'(?:به\s+مبلغ)\s+(amount-\d+)', r'\1'),
526
+ (r'(?:با\s+ارزش)\s+(amount-\d+)', r'\1'),
527
+ (r'(?:در\s+حد)\s+(amount-\d+)', r'\1'),
528
+
529
+ # حالت 5: فعل + amount (بدون حرف اضافه)
530
+ (r'(?:رسید|رسیده|می\u200cرسد)\s+(amount-\d+)', r'\1'),
531
+ (r'(?:شد|شده|می\u200cشود)\s+(amount-\d+)', r'\1'),
532
+ (r'(?:بود|بوده|است)\s+(amount-\d+)', r'\1'),
533
+ ])
534
+
535
+ if "percent" in self.entities_to_anonymize:
536
+ patterns.extend([
537
+ (r'(?:درصد|%)\s+(percent-\d+)', r'\1'),
538
+ (r'(percent-\d+)\s+(?:درصد|درصدی|%)', r'\1'),
539
+ ])
540
+
541
+ # اعمال ال��وها
542
+ for pattern, replacement in patterns:
543
+ new_text = re.sub(pattern, replacement, cleaned)
544
+ if new_text != cleaned:
545
+ count = len(re.findall(pattern, cleaned))
546
+ changes_made += count
547
+ cleaned = new_text
548
+ logger.info(f" ✅ حذف '{pattern}': {count} مورد")
549
+
550
+ if changes_made > 0:
551
+ logger.info(f"✅ {changes_made} کلمه اضافی حذف شد")
552
+ else:
553
+ logger.info("✅ کلمه اضافی یافت نشد")
554
+
555
+ return cleaned
556
+
557
+ def restore_text(self, anonymized_text: str) -> str:
558
+ """بازگردانی متن با ترتیب بهینه برای amount"""
559
+ logger.info("🔄 بازگردانی متن...")
560
+
561
+ if not self.mapping_table:
562
+ logger.warning("⚠️ جدول نگاشت خالی است")
563
+ return anonymized_text
564
+
565
+ logger.info(f"📋 تعداد موجودیت‌ها در mapping: {len(self.mapping_table)}")
566
+
567
+ # ✅ STEP 1: normalize (hyphen یونیکد و جداسازی کلمات چسبیده)
568
+ restored = self._normalize_tokens(anonymized_text)
569
+
570
+ # ✅ STEP 2: restore قوی مخصوص amount با regex (قبل از clean!)
571
+ # این کلیدی است - باید قبل از clean انجام شود
572
+ logger.info("🔥 بازگردانی amount با regex...")
573
+ amount_restored_count = 0
574
+ for placeholder, original in self.mapping_table.items():
575
+ if placeholder.startswith("amount-"):
576
+ # استخراج شماره
577
+ num = placeholder.split("-")[1]
578
+ # الگوی regex: amount [فاصله اختیاری] - [فاصله اختیاری] شماره
579
+ pattern = rf'amount\s*-\s*{num}'
580
+ matches = re.findall(pattern, restored)
581
+ if matches:
582
+ restored = re.sub(pattern, original, restored)
583
+ amount_restored_count += 1
584
+ logger.info(f"✅ regex: {placeholder} → {original[:30]}...")
585
+
586
+ if amount_restored_count > 0:
587
+ logger.info(f"✅ {amount_restored_count} amount با regex بازگردانی شد")
588
+
589
+ # ✅ STEP 3: clean (حذف کلمات اضافی)
590
+ # حالا که amount ها restore شدن، می‌تونیم clean کنیم
591
+ restored = self._clean_for_restore(restored)
592
+
593
+ # ✅ STEP 4: replace ساده برای بقیه (person, company, percent)
594
+ replacements_count = 0
595
+ for placeholder, original in sorted(self.mapping_table.items(), key=lambda x: len(x[0]), reverse=True):
596
+ # amount ها رو قبلاً restore کردیم
597
+ if placeholder.startswith("amount-"):
598
+ continue
599
+
600
+ if placeholder in restored:
601
+ restored = restored.replace(placeholder, original)
602
+ replacements_count += 1
603
+ logger.info(f"✅ {placeholder} → {original[:30]}...")
604
+ else:
605
+ logger.warning(f"⚠️ {placeholder} در متن یافت نشد!")
606
+
607
+ total_restored = amount_restored_count + replacements_count
608
+ logger.info(f"✅ بازگردانی کامل - {total_restored}/{len(self.mapping_table)} جایگزین شد")
609
+
610
+ # ✅ STEP 5: fallback regex برای توکن‌های باقی‌مانده
611
+ if total_restored < len(self.mapping_table):
612
+ logger.info("🔍 تلاش برای یافتن توکن‌های گم‌شده با regex...")
613
+ restored = self._restore_with_regex(restored)
614
+
615
+ # هشدار در صورت شکست کامل
616
+ if total_restored == 0 and len(self.mapping_table) > 0:
617
+ logger.error("❌ هیچ توکنی جایگزین نشد! متن ورودی احتمالاً متفاوت است.")
618
+
619
+ return restored
620
+
621
+ def _clean_for_restore(self, text: str) -> str:
622
+ """پاکسازی خاص برای بازگردانی (شبیه _clean_llm_response اما سبک‌تر)"""
623
+ logger.info("🧹 پاکسازی قبل از بازگردانی...")
624
+
625
+ cleaned = text
626
+ changes_made = 0
627
+
628
+ patterns = []
629
+
630
+ if "amount" in self.entities_to_anonymize:
631
+ patterns.extend([
632
+ (r'(?:مبلغ|رقم|عدد|قیمت|ارزش|فروش|درآمد|هزینه|سود|زیان)\s+(amount-\d+)', r'\1'),
633
+ (r'\bبه\s+(amount-\d+)', r'\1'),
634
+ (r'\bبا\s+(amount-\d+)', r'\1'),
635
+ (r'\bاز\s+(amount-\d+)', r'\1'),
636
+ (r'\bتا\s+(amount-\d+)', r'\1'),
637
+ ])
638
+
639
+ for pattern, replacement in patterns:
640
+ new_text = re.sub(pattern, replacement, cleaned)
641
+ if new_text != cleaned:
642
+ changes_made += re.subn(pattern, replacement, cleaned)[1]
643
+ cleaned = new_text
644
+
645
+ if changes_made > 0:
646
+ logger.info(f"✅ {changes_made} کلمه اضافی حذف شد")
647
+
648
+ return cleaned
649
+
650
+ def _restore_with_regex(self, text: str) -> str:
651
+ """بازگردانی با استفاده از regex برای پیدا کردن توکن‌های دارای کلمات اضافی"""
652
+ restored = text
653
+
654
+ for placeholder, original in self.mapping_table.items():
655
+ # اگر قبلاً جایگزین شده، رد شو
656
+ if placeholder not in text:
657
+ # الگوی regex: کلمه اضافی (اختیاری) + توکن
658
+ # مثلاً: "فروش amount-01" یا "مبلغ amount-05"
659
+ entity_type = placeholder.split('-')[0]
660
+ entity_num = placeholder.split('-')[1]
661
+
662
+ # الگوهای مختلف
663
+ patterns = [
664
+ # کلمه فارسی + فاصله + توکن
665
+ rf'[ء-ي]+\s+({entity_type}-{entity_num})\b',
666
+ # توکن + فاصله + کلمه فارسی
667
+ rf'\b({entity_type}-{entity_num})\s+[ء-ي]+',
668
+ # فاصله اضافی داخل توکن
669
+ rf'\b{entity_type}\s+-\s+{entity_num}\b',
670
+ ]
671
+
672
+ for pattern in patterns:
673
+ matches = list(re.finditer(pattern, restored))
674
+ if matches:
675
+ logger.info(f"✅ پیدا شد با regex: {pattern}")
676
+ for match in matches:
677
+ # جایگزینی کل عبارت با فقط original
678
+ full_match = match.group(0)
679
+ # اگر توکن داخل match هست، فقط اون رو جایگزین کن
680
+ if placeholder in full_match:
681
+ restored = restored.replace(full_match, full_match.replace(placeholder, original))
682
+ else:
683
+ # اگر فرمت توکن متفاوت بود
684
+ restored = restored.replace(full_match, original)
685
+ logger.info(f"✅ regex: {placeholder} → {original[:30]}...")
686
+ break
687
+
688
+ return restored
689
+
690
+ def _normalize_tokens(self, text: str) -> str:
691
+ """نرمال‌سازی توکن‌ها - حذف فاصله‌های اضافی و hyphen یونیکد"""
692
+ logger.info("🧹 نرمال‌سازی توکن‌ها...")
693
+
694
+ normalized = text
695
+ changes = 0
696
+
697
+ # ✅ 1. نرمال‌سازی hyphen های یونیکد برای همه موجودیت‌ها
698
+ # این hyphen ها: ‐ ‑ ‒ – — − و hyphen معمولی -
699
+ unicode_hyphens = r'[\u2010\u2011\u2012\u2013\u2014\u2212\-]'
700
+
701
+ for entity_type in self.entities_to_anonymize:
702
+ # تبدیل همه hyphen ها به - معمولی
703
+ pattern = rf'{entity_type}{unicode_hyphens}(\d+)'
704
+ replacement = rf'{entity_type}-\1'
705
+ count = len(re.findall(pattern, normalized))
706
+ if count > 0:
707
+ normalized = re.sub(pattern, replacement, normalized)
708
+ changes += count
709
+ logger.info(f" ✅ {entity_type}: {count} hyphen یونیکد نرمال شد")
710
+
711
+ # ✅ 2. حذف فاضله‌های اضافی داخل توکن
712
+ for entity_type in self.entities_to_anonymize:
713
+ pattern = rf'{entity_type}\s+-\s+(\d+)'
714
+ replacement = rf'{entity_type}-\1'
715
+ count = len(re.findall(pattern, normalized))
716
+ if count > 0:
717
+ normalized = re.sub(pattern, replacement, normalized)
718
+ changes += count
719
+ logger.info(f" ✅ {entity_type}: {count} فاصله اضافی حذف شد")
720
+
721
+ # ✅ 3. جدا کردن توکن‌ها از کلمات فارسی چسبیده (ویژه amount)
722
+ # مثال: amount-01در → amount-01 در
723
+ if "amount" in self.entities_to_anonymize:
724
+ pattern = r'(amount-\d+)([ء-ي])'
725
+ replacement = r'\1 \2'
726
+ before = normalized
727
+ normalized = re.sub(pattern, replacement, normalized)
728
+ if normalized != before:
729
+ count = len(re.findall(pattern, before))
730
+ changes += count
731
+ logger.info(f" ✅ amount: {count} کلمه چسبیده جدا شد")
732
+
733
+ # ✅ 4. جدا کردن توکن‌ها از نشانه‌گذاری (ویژه amount)
734
+ # مثال: amount-01، → amount-01 ،
735
+ if "amount" in self.entities_to_anonymize:
736
+ pattern = r'(amount-\d+)([،؛:.!?])'
737
+ replacement = r'\1 \2'
738
+ before = normalized
739
+ normalized = re.sub(pattern, replacement, normalized)
740
+ if normalized != before:
741
+ count = len(re.findall(pattern, before))
742
+ changes += count
743
+ logger.info(f" ✅ amount: {count} نشانه‌گذاری جدا شد")
744
+
745
+ if changes > 0:
746
+ logger.info(f"✅ مجموع {changes} تغییر نرمال‌سازی")
747
+
748
+ return normalized
749
+
750
+ def get_mapping_table_md(self) -> str:
751
+ """تبدیل جدول نگاشت به Markdown"""
752
+ if not self.mapping_table:
753
+ return "### 📋 جدول نگاشت\n\nهیچ موجودیتی شناسایی نشد"
754
+
755
+ table = "### 📋 جدول نگاشت\n\n"
756
+ table += "| شناسه | متن اصلی |\n"
757
+ table += "|-------|----------|\n"
758
+
759
+ for token, original in sorted(self.mapping_table.items()):
760
+ table += f"| **{token}** | {original} |\n"
761
+
762
+ return table
763
+
764
+ # متغیر سراسری
765
+ anonymizer = None
766
+
767
+ def process(
768
+ input_text: str,
769
+ analysis_prompt: str,
770
+ llm_provider: str,
771
+ llm_model: str,
772
+ anonymize_all: bool,
773
+ anonymize_person: bool,
774
+ anonymize_company: bool,
775
+ anonymize_amount: bool,
776
+ anonymize_percent: bool
777
+ ):
778
+ """پردازش متن - 4 مرحله"""
779
+ global anonymizer
780
+
781
+ if not input_text.strip():
782
+ return "", "", "", ""
783
+
784
+ # ✅ ساخت لیست موجودیت‌های انتخابی
785
+ if anonymize_all:
786
+ entities = ["person", "company", "amount", "percent"]
787
+ else:
788
+ entities = []
789
+ if anonymize_person:
790
+ entities.append("person")
791
+ if anonymize_company:
792
+ entities.append("company")
793
+ if anonymize_amount:
794
+ entities.append("amount")
795
+ if anonymize_percent:
796
+ entities.append("percent")
797
+
798
+ # اگه هیچی انتخاب نشده
799
+ if not entities:
800
+ return "", "❌ لطفاً حداقل یک موجودیت برای ناشناس‌سازی انتخاب کنید", "", ""
801
+
802
+ cerebras_key = os.getenv("CEREBRAS_API_KEY")
803
+
804
+ # ایجاد یا آپدیت anonymizer
805
+ if not anonymizer:
806
+ anonymizer = AnonymizerAdvanced(
807
+ cerebras_key,
808
+ llm_provider=llm_provider,
809
+ llm_model=llm_model,
810
+ entities_to_anonymize=entities
811
+ )
812
+ else:
813
+ anonymizer.set_llm_provider(llm_provider, llm_model, entities)
814
+ anonymizer.mapping_table = {}
815
+ anonymizer.reverse_mapping = {}
816
+
817
+ try:
818
+ logger.info("=" * 70)
819
+ logger.info(f"🚀 شروع پردازش - LLM: {llm_provider} ({llm_model})")
820
+ logger.info(f"🎯 موجودیت‌های انتخابی: {', '.join(entities)}")
821
+ logger.info("=" * 70)
822
+
823
+ # مرحله 1: ناشناس‌سازی
824
+ logger.info("🔐 مرحله 1: ناشناس‌سازی...")
825
+ anonymized_text, _ = anonymizer.anonymize_with_cerebras(input_text)
826
+ logger.info(f"✅ ناشناس‌سازی: {len(anonymized_text)} کاراکتر")
827
+
828
+ # ✅ دیباگ: بررسی توکن‌های موجود در متن ناشناس
829
+ logger.info("=" * 70)
830
+ logger.info("🔍 DEBUG - توکن‌های موجود در متن ناشناس:")
831
+ for entity_type in entities:
832
+ tokens_found = re.findall(f'{entity_type}-\\d+', anonymized_text)
833
+ unique_tokens = sorted(set(tokens_found))
834
+ logger.info(f" {entity_type}: {unique_tokens}")
835
+ logger.info("=" * 70)
836
+
837
+ # مرحله 2: LLM (فقط اگر analysis_prompt داده شده باشد)
838
+ has_analysis = analysis_prompt and analysis_prompt.strip()
839
+
840
+ if has_analysis:
841
+ logger.info(f"🤖 مرحله 2: {llm_provider.upper()}...")
842
+ llm_response = anonymizer.analyze_with_llm(anonymized_text, analysis_prompt)
843
+ logger.info(f"✅ {llm_provider.upper()}: {len(llm_response)} کاراکتر")
844
+ else:
845
+ logger.info("⚠️ مرحله 2: بدون تحلیل LLM (پرامپت خالی)")
846
+ llm_response = "⚠️ هیچ دستور تحلیل داده نشده است"
847
+
848
+ # مرحله 3: بازگردانی
849
+ logger.info("🔄 مرحله 3: بازگردانی...")
850
+
851
+ # ✅ اصلاح: اگر تحلیل انجام نشده، متن ناشناس اصلی رو restore کن
852
+ if has_analysis:
853
+ # اگر LLM تحلیل کرده، خروجی LLM رو restore کن
854
+ restored_text = anonymizer.restore_text(llm_response)
855
+ else:
856
+ # اگر تحلیل نشده، متن ناشناس اصلی رو restore کن
857
+ restored_text = anonymizer.restore_text(anonymized_text)
858
+
859
+ logger.info("✅ بازگردانی کامل")
860
+
861
+ # مرحله 4: جدول نگاشت
862
+ logger.info("📋 مرحله 4: جدول نگاشت...")
863
+ mapping_str = anonymizer.get_mapping_table_md()
864
+ logger.info(f"✅ {len(anonymizer.mapping_table)} موجودیت")
865
+
866
+ logger.info("=" * 70)
867
+ logger.info("✅ تمام مراحل کامل!")
868
+ logger.info("=" * 70)
869
+
870
+ return restored_text, llm_response, anonymized_text, mapping_str
871
+
872
+ except Exception as e:
873
+ logger.error(f"❌ خطا: {str(e)}", exc_info=True)
874
+ return "", f"❌ خطا: {str(e)}", "", ""
875
+
876
+ def clear_all():
877
+ """پاک کردن همه"""
878
+ return "", "", "", "", "", "", True, False, False, False, False
879
+
880
+ # Gradio Interface
881
+ css_rtl = """
882
+ .input-box {
883
+ direction: rtl;
884
+ text-align: right;
885
+ }
886
+ .textbox textarea {
887
+ direction: rtl;
888
+ text-align: right;
889
+ font-family: 'Tahoma', serif;
890
+ }
891
+ .thick-divider {
892
+ border-top: 2px solid #333;
893
+ margin: 10px 0;
894
+ }
895
+ .compact-group {
896
+ margin: 0;
897
+ padding: 0;
898
+ }
899
+ .compact-checkbox label {
900
+ padding: 5px 10px !important;
901
+ margin: 3px 0 !important;
902
+ font-size: 0.95em !important;
903
+ }
904
+ """
905
+
906
+ with gr.Blocks(title="سیستم ناشناس‌سازی متون", theme=gr.themes.Soft(), css=css_rtl) as app:
907
+
908
+ gr.Markdown("# 🔐 پلتفرم امن چت با مدل‌های متنوع و ناشناس‌سازی داده‌ها", elem_classes="input-box")
909
+
910
+ # ردیف اول: تنظیمات مدل و انتخاب موجودیت‌ها
911
+ with gr.Row():
912
+ # سمت راست: تنظیمات مدل
913
+ with gr.Column(scale=1):
914
+ with gr.Group():
915
+ gr.Markdown("### ⚙️ تنظیمات مدل", elem_classes="input-box")
916
+
917
+ llm_provider = gr.Dropdown(
918
+ choices=["chatgpt", "grok"],
919
+ value="chatgpt",
920
+ label="🤖 انتخاب مدل زبانی",
921
+ interactive=True
922
+ )
923
+
924
+ llm_model = gr.Dropdown(
925
+ choices=AVAILABLE_MODELS["chatgpt"],
926
+ value="gpt-4o-mini",
927
+ label="📦 انتخاب نسخه مدل",
928
+ interactive=True
929
+ )
930
+
931
+ # سمت چپ: انتخاب موجودیت‌ها
932
+ with gr.Column(scale=1):
933
+ with gr.Group():
934
+ gr.Markdown("### 🎯 انتخاب موجودیت‌ها", elem_classes="input-box")
935
+
936
+ anonymize_all = gr.Checkbox(
937
+ label="✅ همه موجودیت‌ها",
938
+ value=True,
939
+ elem_classes="input-box compact-checkbox"
940
+ )
941
+
942
+ anonymize_person = gr.Checkbox(
943
+ label="👤 اسامی اشخاص",
944
+ value=False,
945
+ elem_classes="input-box compact-checkbox"
946
+ )
947
+
948
+ anonymize_company = gr.Checkbox(
949
+ label="🏢 نام شرکت‌ها",
950
+ value=False,
951
+ elem_classes="input-box compact-checkbox"
952
+ )
953
+
954
+ anonymize_amount = gr.Checkbox(
955
+ label="💰 ارقام مالی",
956
+ value=False,
957
+ elem_classes="input-box compact-checkbox"
958
+ )
959
+
960
+ anonymize_percent = gr.Checkbox(
961
+ label="📊 درصدها",
962
+ value=False,
963
+ elem_classes="input-box compact-checkbox"
964
+ )
965
+
966
+ # خط جداکننده پررنگ
967
+ gr.Markdown("---", elem_classes="thick-divider")
968
+
969
+ # ردیف دوم: دستورات پردازش و متن ورودی
970
+ with gr.Row():
971
+ # سمت راست: دستورات پردازش
972
+ with gr.Column(scale=1):
973
+ gr.Markdown("### 📋 دستورات پردازش", elem_classes="input-box")
974
+
975
+ analysis_prompt = gr.Textbox(
976
+ lines=22,
977
+ placeholder="مثال: این متن را خلاصه کن\nیا: نکات کلیدی را استخراج کن",
978
+ label="📋 دستورات LLM (اختیاری)",
979
+ elem_classes="textbox"
980
+ )
981
+
982
+ # سمت چپ: متن ورودی
983
+ with gr.Column(scale=1):
984
+ gr.Markdown("### 📝 متن ورودی", elem_classes="input-box")
985
+
986
+ input_text = gr.Textbox(
987
+ lines=22,
988
+ placeholder="متن مالی/خبری را وارد کنید...",
989
+ label="",
990
+ elem_classes="textbox"
991
+ )
992
+
993
+ # دکمه‌های پردازش و پاک کردن
994
+ with gr.Row():
995
+ process_btn = gr.Button(
996
+ "▶️ پردازش",
997
+ variant="primary",
998
+ size="lg",
999
+ scale=2
1000
+ )
1001
+
1002
+ clear_btn = gr.Button(
1003
+ "🗑️ پاک کردن",
1004
+ variant="stop",
1005
+ size="lg",
1006
+ scale=1
1007
+ )
1008
+
1009
+ # نتایج
1010
+ gr.Markdown("## 📊 نتایج پردازش", elem_classes="input-box")
1011
+
1012
+ with gr.Row():
1013
+ with gr.Column(scale=1):
1014
+ restored_text = gr.Textbox(
1015
+ lines=12,
1016
+ label="✅ متن بازگردانی شده",
1017
+ interactive=False,
1018
+ elem_classes="textbox"
1019
+ )
1020
+
1021
+ with gr.Column(scale=1):
1022
+ llm_analysis = gr.Textbox(
1023
+ lines=12,
1024
+ label="🤖 تحلیل LLM",
1025
+ interactive=False,
1026
+ elem_classes="textbox"
1027
+ )
1028
+
1029
+ with gr.Column(scale=1):
1030
+ anonymized_text = gr.Textbox(
1031
+ lines=12,
1032
+ label="🔒 متن ناشناس‌شده",
1033
+ interactive=False,
1034
+ elem_classes="textbox"
1035
+ )
1036
+
1037
+ mapping_table = gr.Markdown(
1038
+ value="### 📋 جدول نگاشت\n\nهنوز پردازشی انجام نشده",
1039
+ label="📋 جدول نگاشت",
1040
+ elem_classes="input-box"
1041
+ )
1042
+
1043
+
1044
+ # Event Handler برای تغییر provider
1045
+ def handle_provider_change(provider):
1046
+ models = AVAILABLE_MODELS.get(provider, [])
1047
+ default_model = models[0] if models else None
1048
+ return gr.update(choices=models, value=default_model)
1049
+
1050
+ llm_provider.change(
1051
+ fn=handle_provider_change,
1052
+ inputs=[llm_provider],
1053
+ outputs=[llm_model]
1054
+ )
1055
+
1056
+ def handle_select_all(select_all):
1057
+ if select_all:
1058
+ return (
1059
+ gr.update(value=False, interactive=False),
1060
+ gr.update(value=False, interactive=False),
1061
+ gr.update(value=False, interactive=False),
1062
+ gr.update(value=False, interactive=False)
1063
+ )
1064
+ else:
1065
+ return (
1066
+ gr.update(value=False, interactive=True),
1067
+ gr.update(value=False, interactive=True),
1068
+ gr.update(value=False, interactive=True),
1069
+ gr.update(value=False, interactive=True)
1070
+ )
1071
+
1072
+ anonymize_all.change(
1073
+ fn=handle_select_all,
1074
+ inputs=[anonymize_all],
1075
+ outputs=[anonymize_person, anonymize_company, anonymize_amount, anonymize_percent]
1076
+ )
1077
+
1078
+ # پردازش
1079
+ process_btn.click(
1080
+ fn=process,
1081
+ inputs=[
1082
+ input_text,
1083
+ analysis_prompt,
1084
+ llm_provider,
1085
+ llm_model,
1086
+ anonymize_all,
1087
+ anonymize_person,
1088
+ anonymize_company,
1089
+ anonymize_amount,
1090
+ anonymize_percent
1091
+ ],
1092
+ outputs=[restored_text, llm_analysis, anonymized_text, mapping_table]
1093
+ )
1094
+
1095
+ # پاک کردن
1096
+ clear_btn.click(
1097
+ fn=clear_all,
1098
+ outputs=[
1099
+ input_text,
1100
+ analysis_prompt,
1101
+ restored_text,
1102
+ llm_analysis,
1103
+ anonymized_text,
1104
+ mapping_table,
1105
+ anonymize_all,
1106
+ anonymize_person,
1107
+ anonymize_company,
1108
+ anonymize_amount,
1109
+ anonymize_percent
1110
+ ]
1111
+ )
1112
+
1113
+ if __name__ == "__main__":
1114
+ print("=" * 70)
1115
+ print("🚀 سیستم ناشناس‌سازی متون در حال راه‌اندازی...")
1116
+ print("=" * 70)
1117
+ print("\n📋 نحوه استفاده:\n")
1118
+ print("1. API Keyها را در Hugging Face Secrets تنظیم کنید:")
1119
+ print(" - CEREBRAS_API_KEY (ضروری برای ناشناس‌سازی)")
1120
+ print(" - OPENAI_API_KEY (برای ChatGPT)")
1121
+ print(" - XAI_API_KEY (برای Grok)")
1122
+ print("2. http://localhost:7860 را باز کنید")
1123
+ print("3. مدل زبانی (ChatGPT/Grok) و نسخه مدل را انتخاب کنید")
1124
+ print("4. موجودیت‌های مورد نظر برای ناشناس‌سازی را انتخاب کنید")
1125
+ print("5. متن و دستورات پردازش را وارد کنید")
1126
+ print("6. 'پردازش' را کلیک کنید\n")
1127
+ print("🔐 تمام API Keyها از Hugging Face Secrets خوانده می‌شوند")
1128
+ print("📦 مدل‌های پشتیبانی شده:")
1129
+ print(" • ChatGPT GPT-5: gpt-5.1, gpt-5")
1130
+ print(" • ChatGPT GPT-4: gpt-4.1, gpt-4o, gpt-4o-mini, gpt-4-turbo")
1131
+ print(" • Grok-4: grok-4-fast-reasoning, grok-4-fast-non-reasoning, grok-4-0709")
1132
+ print(" • Grok-3: grok-3, grok-3-mini")
1133
+ print(" • Grok-2: grok-2-vision-1212, grok-2-1212, grok-2")
1134
+ print("=" * 70 + "\n")
1135
+
1136
+ app.launch(
1137
+ server_name="0.0.0.0",
1138
+ server_port=7860,
1139
+ share=False,
1140
+ show_error=True
1141
+ )