""" ناشناس‌ساز پیشرفته متون مالی/خبری فارسی نسخه: 1.3.0 - با پشتیبانی از Qwen3 """ import requests import json import gradio as gr from typing import Dict, Any, Optional import os from dataclasses import dataclass import re @dataclass class OpenRouterConfig: """تنظیمات OpenRouter API""" api_key: str base_url: str = "https://openrouter.ai/api/v1" max_tokens: int = 8192 temperature: float = 0.6 top_p: float = 0.85 class AdvancedOpenRouterAnonymizer: """سیستم پیشرفته ناشناس‌سازی""" # لیست مدل‌های رایگان با اولویت (بروز شده) AVAILABLE_MODELS = [ # Qwen3 - بهترین برای فارسی "qwen/qwen3-30b-a3b:free", ] def __init__(self, api_key: str = None, preferred_model: str = None): if api_key is None: api_key = os.getenv("OPENROUTER_API_KEY") if not api_key: raise ValueError("کلید API یافت نشد") self.config = OpenRouterConfig(api_key=api_key) self.preferred_model = preferred_model self.working_model = None self.system_prompt = self._create_system_prompt() def _create_system_prompt(self) -> str: """دستورالعمل سیستمی بهینه شده""" return """شما یک سیستم ناشناس‌سازی متون مالی/خبری فارسی هستید. ## وظیفه: موجودیت‌های حساس را با placeholder های اندیس‌دار جایگزین کنید. ## **قوانین اندیس‌گذاری - CRITICAL** ### **1. ترتیب شماره‌گذاری الزامی:** - شرکت‌ها: company-01, company-02, company-03, company-04, ... (پیوسته و بدون گپ) - اشخاص: person-01, person-02, person-03, ... (پیوسته و بدون گپ) - اعداد: amount-01, amount-02, amount-03, ... (پیوسته و بدون گپ) - درصدها: percent-01, percent-02, percent-03, ... (پیوسته و بدون گپ) ### **2. ثبات شناسه‌ها در متن:** - اگر "همراه اول" اول‌بار company-01 شد، در تمام متن همان باشد - اگر "مهدی احمدی" اول‌بار person-01 شد، در تمام متن همان باشد ### **3. تشخیص صحیح انواع:** **شرکت/سازمان:** همراه اول، بانک ملی، ایران‌خودرو، سایپا، بانک مرکزی، سامانه کدال، وزارت نفت، سازمان تنظیم مقررات رادیویی، سازمان تامین اجتماعی **⚠️ CRITICAL - گروه‌ها:** "گروه همراه اول"، "گروه اقتصادی آزادگان"، "گروه مالی صبا" → همه company-XX هستند (نه group-XX) **⚠️ CRITICAL - کلمات عمومی:** "سه شرکت دارویی"، "چند بانک"، "یک شرکت" → کلمات عمومی هستند، موجودیت نیستند (حفظ شوند) **⚠️ CRITICAL - نام‌های مستعار:** "فاما" همان "فولاد مبارکه اصفهان" است → هر دو company-01 **شخص:** مهدی اخوان بهابادی، محمدرضا فرزین، ابوالفضل نجارزاده **عدد:** 37، 70، 677، 73.7، 178 (هر عددی) **درصد:** 37 درصدی، 15 درصدی، 53 درصد، 43% ## **⚠️ CRITICAL - قوانین پیشرفته ناشناس‌سازی:** ### **1. حفظ هویت شرکت در کل متن (بسیار مهم):** - اگر "شرکت پتروشیمی بوعلی سینا" را company-01 کردی، در ادامه متن "این شرکت"، "بوعلی"، "شرکت مذکور" همه باید همان company-01 باشند - **اشتباه رایج:** "company-01 ... این شرکت company-02" ❌ - **صحیح:** "company-01 ... این شرکت company-01" ✅ ### **2. بازرس/حسابرس = شرکت است (نه شخص):** - "شرکت وانیا نیک تدبیر را به‌عنوان بازرس قانونی انتخاب کردند" → company-XX - "حسابرس" و "بازرس" اسم شرکت‌های حسابرسی است → company-XX (نه person-XX) ### **3. واحدها را حفظ کن (CRITICAL):** - "amount-01 دستگاه محصول" ✅ (واحد حفظ شود) - "amount-01 محصول" ❌ (واحد حذف شده) - "amount-03 همت" ✅ - "amount-03" ❌ - "amount-05 گیگابیت بر ثانیه" ✅ ### **4. اعداد خاص را موجودیت نگیر:** - شماره ثبت: "شماره 11385" → حفظ شود ❌ amount-XX نشود - شماره تماس، کد ملی، شماره حساب → حفظ شوند - فقط مبالغ پولی، تعداد، وزن → amount-XX ### **5. درصدهای دقیق را حفظ کن:** - "99.99 درصد" → حفظ شود (نه percent-XX) - درصدهای با اعشار بسیار دقیق مثل 99.99، 0.01 → حفظ شوند - درصدهای معمولی: "40 درصد" → percent-01 ### **6. نهادها و مراجع عمومی:** - "سازمان بورس و اوراق بهادار" → company-XX ✅ - "مرجع ثبت شرکتها" → حفظ شود ❌ (مرجع عمومی است) - "هیئت تحقیق و تفحص مجلس" → حفظ شود ❌ (نهاد عمومی) - "سامانه کدال" → company-XX ✅ (سامانه خاص) ### **7. کلمات توصیفی عمومی را موجودیت نگیر:** - "سه خودروساز بزرگ کشور" → حفظ شود ❌ - "یک شرکت سرمایه‌گذاری دولتی" → حفظ شود ❌ - "19 بانکی که اطلاعات" → "19 بانکی" حفظ شود ❌ - فقط اگر نام خاص داشت: "شرکت سرمایه‌گذاری ملی" → company-XX ✅ ### **8. صنعت/بخش/حوزه را موجودیت نگیر:** - "صنعت پالایش" → حفظ شود ❌ (نه industry-XX) - "بخش خصوصی" → حفظ شود ❌ - "حوزه بازار سرمایه" → حفظ شود ❌ ### **9. دوره‌های زمانی با "حدود":** - "حدود 18 تا 24 ماه" → حفظ شود ❌ (نه amount-XX) - "حدود 2 سال" → حفظ شود ❌ ### **10. بازه‌های عددی - یک entity:** - "یک تا 1.5 میلیون تن" → amount-01 ✅ (یک entity) - "40 الی 60 درصد" → percent-01 الی percent-02 ❌ - بازه = یک entity واحد ### **11. مقایسه با خود شرکت:** - "company-01 ... بیشتر از company-01" → اشتباه! - اگر شرکت با دوره قبل خود مقایسه می‌شود → amount متفاوت ولی company یکسان ## **تشخیص دقیق درصدها:** - "37 درصدی" → percent-01 (نه amount) - "15 درصد" → percent-02 (نه amount) - "53%" → percent-03 (نه amount) - "بازه 10 تا 20 درصد" → percent-04 تا percent-05 - «رنج‌ها» با «تا/الی/بین … و …» باید یک entity واحد باشند: مثال: «یک تا 1.5 میلیون تن» → یک amount-# ، «50 الی 70 درصد» → یک percent-# . ## **⚠️ CRITICAL - دوره‌های زمانی را حفظ کن:** - "۹ ماهه" → حفظ شود (نه amount-XX) - "۵ ماهه سال" → حفظ شود (نه amount-XX) - "۳ ماهه اول" → حفظ شود (نه amount-XX) - "۶ ماهه منتهی به" → حفظ شود (نه amount-XX) - "سه‌ماهه نخست" → حفظ شود (نه amount-XX) - "در ۹ ماه" → "در ۹ ماه" حفظ شود - "عملکرد ۵ ماهه" → "عملکرد ۵ ماهه" حفظ شود - "حدود 18 تا 24 ماه" → حفظ شود (بازه زمانی) - "حدود 2 سال" → حفظ شود اما: - "۹ ماه سپرده" → "amount-XX ماه سپرده" (چون مدت سپرده است) - "۹ میلیون تومان" → amount-XX (چون مبلغ است) **قانون:** اگر عدد + "ماهه" یا "ماهه سال" یا "ماهه اول" باشد → حفظ کن **قانون:** اگر عدد + "ماه" بدون "ه" باشد و منظور تعداد ماه است → amount-XX - تاریخ/ماه/سال و ساعت را فعلاً «اصلاً» انتیتی نگیر (هیچ date-* / time-* تولید نکن). ## **موارد حفظ شده:** - تاریخ‌ها: 1404/04/23، 30 آذر 1403، پاییز 1401 - فصل‌های سال: پاییز، بهار، تابستان، زمستان (حفظ شوند، موجودیت نیستند) - عناوین شغلی: مدیرعامل، رئیس کل، مدیرکل - واحدها: میلیارد تومان، همت، ریال، ماه، سال - مکان‌ها: تهران، اصفهان، ایران - کلمات عمومی: "سه شرکت دارویی"، "چند بانک"، "یک شرکت"، "مراکز درمانی" (بدون نام خاص) - ⚠️ **CRITICAL - دوره‌های زمانی:** "۵ ماهه سال"، "۹ ماهه"، "۳ ماهه اول"، "۶ ماهه منتهی به" → حفظ شوند (نه amount-XX) ## **ممنوع:** - کلمات انگلیسی اضافی - تغییر ساختار جمله - حذف یا اضافه کردن کلمات - ⚠️ **CRITICAL: استفاده از group-XX ممنوع است** - همه گروه‌ها باید company-XX باشند **فقط متن ناشناس‌شده را برگردان - هیچ توضیح اضافی نیاز نیست.**""" def _try_model(self, model: str, text: str) -> Optional[Dict[str, Any]]: """تست یک مدل""" headers = { "Authorization": f"Bearer {self.config.api_key}", "Content-Type": "application/json" } payload = { "model": model, "messages": [ {"role": "system", "content": self.system_prompt}, {"role": "user", "content": f"متن فارسی را ناشناس کن:\n\n{text}"} ], "temperature": self.config.temperature, "top_p": self.config.top_p, "max_tokens": self.config.max_tokens } try: response = requests.post( f"{self.config.base_url}/chat/completions", headers=headers, json=payload, timeout=60 ) if response.status_code == 200: return response.json() elif response.status_code == 404: print(f"⚠️ {model}: مدل موجود نیست (404)") return None else: print(f"⚠️ {model}: خطا {response.status_code}") return None except Exception as e: print(f"⚠️ {model}: {str(e)}") return None def _make_api_request(self, text: str) -> Dict[str, Any]: """ارسال درخواست با fallback هوشمند""" # اگر قبلاً مدل کاری پیدا کردیم if self.working_model: print(f"🔄 استفاده از مدل قبلی: {self.working_model}") result = self._try_model(self.working_model, text) if result: return result else: print("⚠️ مدل قبلی کار نکرد، جستجوی مدل جدید...") self.working_model = None # مدل ترجیحی if self.preferred_model: print(f"🎯 تست مدل ترجیحی: {self.preferred_model}") result = self._try_model(self.preferred_model, text) if result: self.working_model = self.preferred_model return result # امتحان تمام مدل‌ها print("🔍 جستجوی مدل کاری...") for i, model in enumerate(self.AVAILABLE_MODELS, 1): print(f"[{i}/{len(self.AVAILABLE_MODELS)}] در حال تست {model}...") result = self._try_model(model, text) if result: print(f"✅ موفق با {model}") self.working_model = model return result # اگر هیچ مدلی کار نکرد raise Exception( "❌ هیچ مدلی در دسترس نیست!\n\n" "🔍 بررسی کنید:\n" "1. کلید API معتبر است؟\n" "2. اتصال اینترنت فعال است؟\n" "3. OpenRouter سرویس می‌دهد؟\n\n" "💡 راهکار:\n" "- به https://openrouter.ai/keys بروید\n" "- کلید جدید بگیرید\n" "- مجدداً تلاش کنید" ) def anonymize_text(self, text: str) -> Dict[str, Any]: """ناشناس‌سازی متن""" if not text.strip(): return {"success": False, "error": "متن ورودی خالی است"} try: response = self._make_api_request(text) if "choices" not in response or not response["choices"]: return {"success": False, "error": "پاسخ نامعتبر"} content = response["choices"][0]["message"]["content"] content = self._clean_output(content) if not content: return {"success": False, "error": "خروجی خالی است"} analysis = self._analyze_anonymized_text(content) return { "success": True, "anonymized_text": content, "entities": analysis["entities"], "statistics": analysis["statistics"], "usage": response.get("usage", {}), "model_used": self.working_model or "unknown" } except Exception as e: return {"success": False, "error": str(e)} def _clean_output(self, content: str) -> str: """پاک‌سازی خروجی""" # حذف markdown if "```" in content: lines = content.split('\n') clean_lines = [] skip = False for line in lines: if line.strip().startswith('```'): skip = not skip continue if not skip: clean_lines.append(line) content = '\n'.join(clean_lines) # حذف خطوط انگلیسی/توضیحات lines = content.split('\n') persian_lines = [] for line in lines: line = line.strip() if not line: continue # نگه داشتن خطوطی که فارسی یا entity دارند has_persian = any(c in 'ابپتثجچحخدذرزژسشصضطظعغفقکگلمنوهیآأإئؤة' for c in line) has_entity = re.search(r'(company|person|amount|percent)-\d+', line) if has_persian or has_entity: persian_lines.append(line) return '\n'.join(persian_lines).strip() def _analyze_anonymized_text(self, text: str) -> Dict[str, Any]: """تحلیل متن ناشناس‌سازی شده""" companies = re.findall(r'company-(\d+)', text) persons = re.findall(r'person-(\d+)', text) amounts = re.findall(r'amount-(\d+)', text) percents = re.findall(r'percent-(\d+)', text) return { "statistics": { "company": len(set(companies)), "person": len(set(persons)), "amount": len(set(amounts)), "percent": len(set(percents)), "total": len(companies) + len(persons) + len(amounts) + len(percents) }, "entities": { "companies": sorted(list(set(companies)), key=lambda x: int(x)) if companies else [], "persons": sorted(list(set(persons)), key=lambda x: int(x)) if persons else [], "amounts": sorted(list(set(amounts)), key=lambda x: int(x)) if amounts else [], "percents": sorted(list(set(percents)), key=lambda x: int(x)) if percents else [] } } def create_interface(): """رابط کاربری Gradio""" api_key_available = bool(os.getenv("OPENROUTER_API_KEY")) with gr.Blocks( title="ناشناس‌ساز فارسی", theme=gr.themes.Soft(), css=".rtl { direction: rtl; text-align: right; }" ) as interface: gr.Markdown(""" # 🔒 ناشناس‌ساز متون مالی/خبری فارسی ### 🚀 پشتیبانی از Qwen3 + Llama 3.2 (OpenRouter) """) if not api_key_available: gr.Markdown(""" ## ⚠️ نیاز به تنظیمات **مراحل:** 1. به [OpenRouter](https://openrouter.ai/keys) بروید 2. یک کلید API رایگان بگیرید 3. در **Settings → Secrets**: - Name: `OPENROUTER_API_KEY` - Value: کلید شما 4. **Restart** Space """) else: gr.Markdown("✅ **API Key تنظیم شده - سیستم آماده است**") with gr.Row(): with gr.Column(): input_text = gr.Textbox( label="📝 متن ورودی", placeholder="متن خبری یا مالی فارسی خود را اینجا بنویسید...", lines=12, elem_classes="rtl" ) with gr.Row(): clear_btn = gr.Button("🗑️ پاک کردن", size="sm") anonymize_btn = gr.Button( "🔒 ناشناس‌سازی", variant="primary", size="lg" ) with gr.Column(): output_text = gr.Textbox( label="🎯 متن ناشناس‌سازی شده", lines=12, elem_classes="rtl", interactive=True ) info_output = gr.Markdown(label="📊 اطلاعات") def process_text(text: str): """پردازش متن""" if not text or not text.strip(): return "", "⚠️ **لطفاً متنی وارد کنید**" try: anonymizer = AdvancedOpenRouterAnonymizer() result = anonymizer.anonymize_text(text) if not result["success"]: error_msg = result['error'] return "", f"❌ **خطا:**\n\n{error_msg}" stats = result.get("statistics", {}) usage = result.get("usage", {}) model = result.get("model_used", "unknown") entities = result.get("entities", {}) # ساخت لیست موجودیت‌ها entity_list = [] if entities.get("companies"): entity_list.append(f"🏢 **شرکت‌ها:** {', '.join([f'company-{i}' for i in entities['companies']])}") if entities.get("persons"): entity_list.append(f"👤 **اشخاص:** {', '.join([f'person-{i}' for i in entities['persons']])}") if entities.get("amounts"): entity_list.append(f"💰 **مبالغ:** {', '.join([f'amount-{i}' for i in entities['amounts']])}") if entities.get("percents"): entity_list.append(f"📊 **درصدها:** {', '.join([f'percent-{i}' for i in entities['percents']])}") entities_display = "\n".join(entity_list) if entity_list else "*هیچ موجودیتی شناسایی نشد*" info_md = f""" ## ✅ ناشناس‌سازی موفق! ### 📊 آمار کلی: - 🏢 **شرکت‌ها:** {stats.get('company', 0)} - 👤 **اشخاص:** {stats.get('person', 0)} - 💰 **مبالغ:** {stats.get('amount', 0)} - 📈 **درصدها:** {stats.get('percent', 0)} - 🔢 **کل جایگزینی‌ها:** {stats.get('total', 0)} ### 🔍 موجودیت‌های شناسایی شده: {entities_display} ### ⚙️ اطلاعات فنی: - 🤖 **مدل:** `{model}` - 📊 **Tokens استفاده شده:** {usage.get('total_tokens', 0)} - ورودی: {usage.get('prompt_tokens', 0)} - خروجی: {usage.get('completion_tokens', 0)} - 💰 **هزینه:** رایگان 🆓 """ return result["anonymized_text"], info_md except Exception as e: return "", f"❌ **خطای غیرمنتظره:**\n\n```\n{str(e)}\n```" def clear_all(): return "", "", "" # Event handlers anonymize_btn.click( fn=process_text, inputs=[input_text], outputs=[output_text, info_output] ) clear_btn.click( fn=clear_all, outputs=[input_text, output_text, info_output] ) # مثال‌ها with gr.Accordion("📚 مثال‌های آماده (کلیک کنید)", open=False): gr.Examples( examples=[ ["همراه اول با سهمی ۵۳ درصدی بیشترین نقش را در بازار دارد."], ["ایران خودرو در اسفند 1402 حدود 23 هزار و 296 میلیارد تومان درآمد کسب کرد که 4.58 درصد افزایش نسبت به سال قبل داشت."], ["علی محمدی مدیرعامل بانک ملت اعلام کرد که این بانک سود 15 درصدی داشته و 500 میلیون تومان سرمایه‌گذاری جدید انجام داده است."], ["فولاد مبارکه اصفهان با نماد فاما در بورس، رشد 8.5 درصدی قیمت سهام را تجربه کرد و به ارزش 12 هزار میلیارد تومان رسید. مهدی رضایی تحلیلگر بازار سرمایه پیش‌بینی می‌کند که این روند ادامه یابد."] ], inputs=input_text, label="مثال‌ها" ) # راهنما with gr.Accordion("📖 راهنمای کامل", open=False): gr.Markdown(""" ## 🎯 نحوه استفاده 1. **وارد کردن متن:** متن خبری یا مالی فارسی را در کادر ورودی بنویسید 2. **کلیک روی دکمه ناشناس‌سازی:** سیستم خودکار شروع به کار می‌کند 3. **دریافت نتیجه:** متن ناشناس‌سازی شده + آمار کامل --- ## 🔍 انواع موجودیت‌ها | نوع | توضیح | مثال | |-----|-------|------| | **company-XX** | شرکت‌ها، بانک‌ها، سازمان‌ها | همراه اول → company-01 | | **person-XX** | نام افراد | علی محمدی → person-01 | | **amount-XX** | مبالغ مالی (با واحد) | 500 میلیون → amount-01 | | **percent-XX** | درصدها (با %) | 15 درصد → percent-01 | --- ## ✅ موارد حفظ شده - ✅ تاریخ‌ها (اسفند 1402، 2023/12/01) - ✅ زمان‌ها (10:30، 14 مارس) - ✅ واحدهای پولی (تومان، ریال، میلیارد) - ✅ کلمات عمومی (سه شرکت، دو نفر) - ✅ اعداد غیرحساس --- ## 🤖 مدل‌های پشتیبانی شده سیستم به ترتیب اولویت این مدل‌ها را امتحان می‌کند: 1. **Qwen3-30B** (بهترین برای فارسی) 2. **Qwen3-235B** (قدرتمندترین) 3. **Qwen3-Next-80B** (جدیدترین) 4. **Llama 3.2** (تست شده ✓) 5. سایر مدل‌های رایگان --- ## 💡 نکات مهم - 🆓 **کاملاً رایگان** - بدون هزینه - 🔒 **امن** - بدون ذخیره داده - ⚡ **سریع** - پردازش آنی - 🎯 **دقیق** - شناسایی هوشمند - 🔄 **پایدار** - fallback خودکار --- ## ❓ سوالات متداول **Q: اگر خطا داد چه کنم؟** A: سیستم خودکار مدل‌های دیگر را امتحان می‌کند. اگر باز هم خطا داد، کلید API را بررسی کنید. **Q: چه مدلی بهتر است؟** A: Qwen3-30B یا Qwen3-235B برای فارسی بهترین هستند. **Q: داده‌های من ذخیره می‌شود؟** A: خیر، OpenRouter به طور پیش‌فرض داده‌ها را ذخیره نمی‌کند. """) return interface if __name__ == "__main__": interface = create_interface() interface.launch()