KashefTech/Data-Anonymization
0
1import gradio as gr2import re3import os4import requests5import json6import logging7from typing import Dict, List, Tuple, Optional8from llm_sender_unified import create_llm_sender9 10# ✅ مدلهای موجود - بهروزرسانی نوامبر 202411AVAILABLE_MODELS = {12 "chatgpt": [13 # GPT-5 Series (جدیدترین)14 "gpt-5.1", # بهترین برای کدنویسی و وظایف agentic15 "gpt-5", # مدل reasoning قبلی16 # GPT-4 Series17 "gpt-4.1", # هوشمندترین non-reasoning18 "gpt-4o", # قدرتمند19 "gpt-4o-mini", # سریع و ارزان20 "gpt-4-turbo", # سریعتر از GPT-421 ],22 "grok": [23 # Grok-4 Series (جدیدترین)24 "grok-4-fast-reasoning", # سریع با reasoning25 "grok-4-fast-non-reasoning", # سریع بدون reasoning26 "grok-4-0709", # نسخه پایدار27 # Grok-3 Series28 "grok-3", # قدرتمند29 "grok-3-mini", # سبک30 # Grok-2 Series31 "grok-2-vision-1212", # با قابلیت بینایی32 "grok-2-1212", # نسخه پایدار33 "grok-2" # نسخه قدیمی34 ]35}36 37logging.basicConfig(level=logging.INFO)38logger = logging.getLogger(__name__)39 40class 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] = None49 ):50 self.cerebras_key = cerebras_key or os.getenv("CEREBRAS_API_KEY")51 self.llm_provider = llm_provider52 self.llm_model = llm_model53 self.entities_to_anonymize = entities_to_anonymize or ["person", "company", "amount", "percent"]54 self.mapping_table = {}55 self.reverse_mapping = {}56 57 # ایجاد LLM sender58 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 = None74 logger.warning("⚠️ Provider ناشناخته")75 76 # ایجاد sender77 self.llm_sender = create_llm_sender(78 provider=self.llm_provider,79 api_key=api_key,80 model=self.llm_model81 )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 ChatGPT88 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 = provider93 self.llm_model = model94 if entities is not None:95 self.entities_to_anonymize = entities96 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 = 1111 112 if "person" in self.entities_to_anonymize:113 instructions.append(f"{instruction_number}. اسامی اشخاص → person-01, person-02, ...")114 instruction_number += 1115 116 if "company" in self.entities_to_anonymize:117 instructions.append(f"{instruction_number}. نام شرکتها/سازمانها → company-01, company-02, ...")118 instruction_number += 1119 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 += 1123 124 if "percent" in self.entities_to_anonymize:125 instructions.append(f"{instruction_number}. درصدها → percent-01, percent-02, ...")126 instruction_number += 1127 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": "llama3.1-8b",165 "messages": [{"role": "user", "content": prompt1}],166 "max_tokens": 4096,167 "temperature": 0.1168 },169 timeout=60170 )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.1228 },229 timeout=60230 )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_table249 250 except Exception as e:251 logger.error(f"❌ Cerebras Exception: {e}")252 raise253 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]) - 1298 299 if token_num < len(original_entities[entity_type]):300 original_text = original_entities[entity_type][token_num]301 self.mapping_table[token] = original_text302 self.reverse_mapping[original_text] = token303 else:304 original_text = original_entities[entity_type][-1]305 if token not in self.mapping_table:306 self.mapping_table[token] = original_text307 self.reverse_mapping[original_text] = token308 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 و Grok326 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-Shot332 few_shot_examples = """333EXAMPLE 1 - CORRECT:334Input: "company-01 فروش amount-01 داشت"335Your output should be EXACTLY: "company-01 فروش amount-01 داشت"336NOT: "company-01 فروش مبلغ amount-01 داشت"337 338EXAMPLE 2 - CORRECT:339Input: "amount-02 به amount-03 رسید"340Your output should be EXACTLY: "amount-02 به amount-03 رسید"341NOT: "مبلغ amount-02 به amount-03 رسید"342 343EXAMPLE 3 - CORRECT:344Input: "company-01 سود percent-01 داشت"345Your output should be EXACTLY: "company-01 سود percent-01 داشت"346NOT: "شرکت 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 365ANONYMIZED TEXT:366{anonymized_text}367 368USER REQUEST:369{analysis_prompt}370 371CRITICAL RULES:3721. Use ONLY these exact tokens: {tokens_str}3732. NEVER add words before/after tokens3743. Keep the EXACT format: amount-01 (not "مبلغ amount-01" or "amount- 01")3754. Do NOT create new tokens3765. Preserve the exact structure377 378{few_shot_examples}379 380FORBIDDEN 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 387Now process the text following these rules EXACTLY."""388 389 try:390 # ✅ temperature خیلی پایین برای GPT-4391 logger.info(f"🌡️ Temperature: 0.05 (GPT-4 ویژه)")392 393 response = self.llm_sender.send(394 combined_text, 395 lang='en', # انگلیسی برای GPT-4396 temperature=0.05, # خیلی خیلی پایین397 max_tokens=2000398 )399 400 # ✅ دیباگ: نمایش خروجی خام LLM401 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 # ✅ دیباگ: نمایش خروجی بعد از clean410 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_response417 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⚠️ قوانین مهم:4541. فقط از کدهای ناشناس موجود استفاده کن: {tokens_str}4552. هیچ کلمهای قبل یا بعد از این کدها اضافه نکن4563. کد جدید ایجاد نکن4574. ساختار دقیق متن را حفظ کن458 459مثالهای صحیح و غلط:460{examples_str}"""461 462 try:463 temp_to_use = 0.2464 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=2000471 )472 473 response = self._clean_llm_response(response)474 475 logger.info(f"✅ {self.llm_provider.upper()}: {len(response)} کاراکتر")476 return response477 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 = text487 changes_made = 0488 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: کلمات قبل از amount508 (r'(?:مبلغ|رقم|عدد|قیمت|ارزش|مقدار)\s+(amount-\d+)', r'\1'),509 (r'(?:فروش|درآمد|سود|زیان|هزینه|خرج)\s+(amount-\d+)', r'\1'),510 (r'(?:دارایی|بدهی|سرمایه|پول|وام)\s+(amount-\d+)', r'\1'),511 512 # حالت 2: حروف اضافه قبل از amount513 (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 += count547 cleaned = new_text548 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 cleaned556 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_text564 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 = 0574 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 += 1584 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 = 0595 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 continue599 600 if placeholder in restored:601 restored = restored.replace(placeholder, original)602 replacements_count += 1603 logger.info(f"✅ {placeholder} → {original[:30]}...")604 else:605 logger.warning(f"⚠️ {placeholder} در متن یافت نشد!")606 607 total_restored = amount_restored_count + replacements_count608 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 restored620 621 def _clean_for_restore(self, text: str) -> str:622 """پاکسازی خاص برای بازگردانی (شبیه _clean_llm_response اما سبکتر)"""623 logger.info("🧹 پاکسازی قبل از بازگردانی...")624 625 cleaned = text626 changes_made = 0627 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_text644 645 if changes_made > 0:646 logger.info(f"✅ {changes_made} کلمه اضافی حذف شد")647 648 return cleaned649 650 def _restore_with_regex(self, text: str) -> str:651 """بازگردانی با استفاده از regex برای پیدا کردن توکنهای دارای کلمات اضافی"""652 restored = text653 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 # جایگزینی کل عبارت با فقط original678 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 break687 688 return restored689 690 def _normalize_tokens(self, text: str) -> str:691 """نرمالسازی توکنها - حذف فاصلههای اضافی و hyphen یونیکد"""692 logger.info("🧹 نرمالسازی توکنها...")693 694 normalized = text695 changes = 0696 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 += count709 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 += count719 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 = normalized727 normalized = re.sub(pattern, replacement, normalized)728 if normalized != before:729 count = len(re.findall(pattern, before))730 changes += count731 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 = normalized739 normalized = re.sub(pattern, replacement, normalized)740 if normalized != before:741 count = len(re.findall(pattern, before))742 changes += count743 logger.info(f" ✅ amount: {count} نشانهگذاری جدا شد")744 745 if changes > 0:746 logger.info(f"✅ مجموع {changes} تغییر نرمالسازی")747 748 return normalized749 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 table763 764# متغیر سراسری765anonymizer = None766 767def 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: bool777):778 """پردازش متن - 4 مرحله"""779 global anonymizer780 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 # ایجاد یا آپدیت anonymizer805 if not anonymizer:806 anonymizer = AnonymizerAdvanced(807 cerebras_key, 808 llm_provider=llm_provider,809 llm_model=llm_model,810 entities_to_anonymize=entities811 )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_str871 872 except Exception as e:873 logger.error(f"❌ خطا: {str(e)}", exc_info=True)874 return "", f"❌ خطا: {str(e)}", "", ""875 876def clear_all():877 """پاک کردن همه"""878 return "", "", "", "", "", "", True, False, False, False, False879 880# Gradio Interface881css_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 906with 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=True922 )923 924 llm_model = gr.Dropdown(925 choices=AVAILABLE_MODELS["chatgpt"],926 value="gpt-4o-mini",927 label="📦 انتخاب نسخه مدل",928 interactive=True929 )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=21000 )1001 1002 clear_btn = gr.Button(1003 "🗑️ پاک کردن",1004 variant="stop",1005 size="lg",1006 scale=11007 )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 برای تغییر provider1045 def handle_provider_change(provider):1046 models = AVAILABLE_MODELS.get(provider, [])1047 default_model = models[0] if models else None1048 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_percent1091 ],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_percent1110 ]1111 )1112 1113if __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=True1141 )