""" ناشناسساز پیشرفته متون مالی/خبری فارسی قدرتگرفته از OpenRouter AI - Qwen3-14B نسخه: 1.0.0 """ import requests import json import gradio as gr from typing import Dict, Any import os from dataclasses import dataclass import re @dataclass class OpenRouterConfig: """تنظیمات OpenRouter API برای Qwen3-14B""" api_key: str base_url: str = "https://openrouter.ai/api/v1" model: str = "qwen/qwen-3-14b:free" # ✅ Qwen3-14B رایگان max_tokens: int = 16384 # ✅ مناسب برای 14B temperature: float = 0.6 # ✅ بهینه برای 14B (0.3 خیلی کم بود) top_p: float = 0.85 # ✅ افزایش برای کیفیت بهتر presence_penalty: float = 0.1 # ✅ کاهش برای طبیعیتر بودن frequency_penalty: float = 0.1 # ✅ کاهش برای طبیعیتر بودن class AdvancedOpenRouterAnonymizer: """سیستم پیشرفته ناشناسسازی متون مالی/خبری فارسی""" def __init__(self, api_key: str = None): # خواندن API key از متغیر محیطی (Secret) if api_key is None: api_key = os.getenv("OPENROUTER_API_KEY") # ✅ امن if not api_key: raise ValueError("کلید API یافت نشد - لطفاً در Settings تنظیم کنید") self.config = OpenRouterConfig(api_key=api_key) self.system_prompt = self._create_advanced_system_prompt() def _create_advanced_system_prompt(self) -> str: """ایجاد دستورالعمل سیستمی پیشرفته""" return """شما یک «ناشناسساز متون مالی/خبری فارسی» هستید. وظیفهتان جایگزینی اسامی خاص و مقادیر عددی با شناسههای بیمعناست. ## قوانین اندیسگذاری: 1. **ترتیب پیوسته**: company-01, company-02, ... | person-01, person-02, ... | amount-01, amount-02, ... | percent-01, percent-02, ... 2. **ثبات**: اگر "همراه اول" → company-01 شد، در تمام متن همان باشد 3. **نام مستعار**: "فاما" = "فولاد مبارکه" → هر دو company-01 4. **اشاره ضمنی**: "این شرکت" اگر به company-01 اشاره دارد → company-01 (نه company-02) ## انواع موجودیت: - **company-XX**: شرکتها، بانکها، سازمانها، گروهها - **person-XX**: نام و نام خانوادگی اشخاص - **amount-XX**: مبالغ - واحد را حفظ کن - **percent-XX**: درصدها ## قوانین کلیدی: 1. بازرس = شرکت است → company-XX 2. واحدها: "amount-01 میلیارد تومان" ✅ 3. گروهها: "گروه X" → company-XX 4. کلمات عمومی حفظ: "سه شرکت" → حفظ 5. دوره زمانی حفظ: "۵ ماهه" → حفظ 6. بازه = یک entity: "یک تا 1.5 میلیون" → amount-01 7. شناسایی و دستهبندی درصدها بین 50 تا 70 درصد به عنوان یک موجودیت درصد در متن 8. شناسایی تمام ارقام موجود در متن به عنوان موجودیت amount-XX مانند "سود خالص 50 میلیارد تومان" را به مقدار amount-01 تبدیل کن 9. شناسایی مقدار درصد در بازه 40–60٪ به عنوان یک موجودیت درصد (percent-03 مثلا) 10. "بزرگترین هلدینگ شستا" را به صورت "بزرگترین هلدینگ company-03" تبدیل کن 11. هر جا که یک شرکت و گروه با نام یکسان ذکر شود، آن را به یک موجودیت یکسان (company-XX) تبدیل کن 12. "سود حاصل از منابع عملیاتی ۱۰،۸۸۷،۸۶۴ میلیون ریال" را به عنوان amount-01 شناسایی کن 13. "بانک ملی ایران" را به عنوان company-01 شناسایی کن 14. "شرکت ارتباطات سیار ایران همراه اول" را به عنوان company-01 شناسایی کن 15. "سپردهگذاری مرکزی اوراق بهادار و تسویه وجوه" را به عنوان company-01 شناسایی کن ## مثال: ورودی: ایران خودرو در اسفند 1402 حدود 23 هزار و 296 میلیارد درآمد کسب کرد که 4.58 درصد افزایش داشت. خروجی: company-01 در اسفند 1402 حدود amount-01 درآمد کسب کرد که percent-01 افزایش داشت. ⚠️ یادآوری: فقط متن ناشناسشده، بدون هیچ توضیح اضافی.""" def _make_api_request(self, text: str) -> Dict[str, Any]: """ارسال درخواست به OpenRouter API""" headers = { "Authorization": f"Bearer {self.config.api_key}", "Content-Type": "application/json", "HTTP-Referer": "https://huggingface.co/spaces", "X-Title": "Persian Text Anonymizer - Qwen3-14B" } payload = { "model": self.config.model, "messages": [ { "role": "system", "content": self.system_prompt }, { "role": "user", "content": text } ], "temperature": self.config.temperature, "top_p": self.config.top_p, "max_tokens": self.config.max_tokens, "presence_penalty": self.config.presence_penalty, "frequency_penalty": self.config.frequency_penalty } try: response = requests.post( f"{self.config.base_url}/chat/completions", headers=headers, json=payload, timeout=90 # ✅ افزایش timeout برای 14B ) response.raise_for_status() return response.json() except requests.exceptions.RequestException as e: raise Exception(f"خطا در ارتباط با OpenRouter API: {str(e)}") def anonymize_text(self, text: str) -> Dict[str, Any]: """ناشناسسازی متن با استفاده از OpenRouter""" 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": "پاسخ نامعتبر از API" } content = response["choices"][0]["message"]["content"] content = self._clean_markdown(content) content = content.strip() analysis = self._analyze_anonymized_text(content) return { "success": True, "anonymized_text": content, "entities": analysis["entities"], "statistics": analysis["statistics"], "detailed_analysis": analysis["detailed_analysis"], "usage": response.get("usage", {}), "quality_check": self._validate_anonymized_text(content) } except Exception as e: return { "success": False, "error": f"خطا در پردازش: {str(e)}" } def _clean_markdown(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) return content 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) statistics = { "company": len(set(companies)), "person": len(set(persons)), "amount": len(set(amounts)), "percent": len(set(percents)), "total_replacements": len(companies) + len(persons) + len(amounts) + len(percents) } entities = { "companies": sorted(list(set(companies)), key=lambda x: int(x)), "persons": sorted(list(set(persons)), key=lambda x: int(x)), "amounts": sorted(list(set(amounts)), key=lambda x: int(x)), "percents": sorted(list(set(percents)), key=lambda x: int(x)) } detailed_analysis = { "preserved_dates": len(re.findall(r'\d{4}/\d{1,2}/\d{1,2}|\d{1,2}\s+\w+\s+\d{4}', text)), "preserved_times": len(re.findall(r'\d{1,2}:\d{2}', text)), "financial_indicators": len(re.findall(r'\b(EPS|P/E|ARPU|NPL|ROE|ROA)\b', text)), "units_preserved": len(re.findall(r'(میلیارد|میلیون|هزار|تومان|ریال|درهم|دلار|یورو|تن|کیلوگرم)', text)) } return { "statistics": statistics, "entities": entities, "detailed_analysis": detailed_analysis } def _validate_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) validation_issues = [] for entity_type, indices in [ ("company", companies), ("person", persons), ("amount", amounts), ("percent", percents) ]: if indices: unique_indices = sorted(list(set([int(x) for x in indices]))) if unique_indices[0] != 1: validation_issues.append(f"اندیس {entity_type} از 01 شروع نشده (شروع: {unique_indices[0]:02d})") expected = list(range(1, len(unique_indices) + 1)) if unique_indices != expected: validation_issues.append(f"اندیسهای {entity_type} پیوسته نیستند") english_words = re.findall(r'\b[a-zA-Z]+\b', text) unwanted_english = [word for word in english_words if word.lower() not in ['eps', 'p/e', 'arpu', 'npl', 'roe', 'roa']] if unwanted_english: validation_issues.append(f"کلمات انگلیسی غیرضروری: {unwanted_english}") return { "is_valid": len(validation_issues) == 0, "issues": validation_issues, "entity_counts": { "company": len(set(companies)), "person": len(set(persons)), "amount": len(set(amounts)), "percent": len(set(percents)) } } def create_advanced_interface(): """ایجاد رابط کاربری پیشرفته""" # بررسی وجود کلید API api_key_available = bool(os.getenv("OPENROUTER_API_KEY")) custom_css = """ .gradio-container { font-family: 'Tahoma', 'Arial', sans-serif !important; direction: rtl; max-width: 1400px; margin: 0 auto; } .result-box { background-color: #f8f9fa; border: 2px solid #e9ecef; border-radius: 12px; padding: 20px; margin: 10px 0; } .warning-box { background-color: #fff3cd; border: 2px solid #ffeaa7; border-radius: 12px; padding: 15px; color: #856404; margin: 10px 0; } .success-box { background-color: #d4edda; border: 2px solid #c3e6cb; border-radius: 12px; padding: 15px; color: #155724; margin: 10px 0; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin: 15px 0; } .stat-card { background-color: #ffffff; border: 1px solid #dee2e6; border-radius: 8px; padding: 15px; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .quality-badge { display: inline-block; padding: 5px 10px; border-radius: 20px; font-weight: bold; margin: 5px; } .quality-pass { background-color: #28a745; color: white; } .quality-fail { background-color: #dc3545; color: white; } """ with gr.Blocks(css=custom_css, title="ناشناسساز پیشرفته با Qwen3-14B", theme=gr.themes.Soft()) as interface: gr.Markdown(""" # 🔒 سیستم پیشرفته ناشناسسازی متون مالی/خبری فارسی ### ⚡ قدرتگرفته از OpenRouter AI ### 🚀 مدل: Qwen3-14B (رایگان!) ### 🎯 دقت بالا در تشخیص موجودیتهای فارسی """) # نمایش وضعیت API if api_key_available: gr.Markdown("""