CoolFace
Apppublic

KashefTech/Data-Anonymization

sourceHugging Facemitupdated 7mo agoView on Hugging Face
0likes
app-llama.py1141 linesDownload Raw Back to root
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    )