""" ناشناس‌ساز پیشرفته متون مالی/خبری فارسی قدرت‌گرفته از 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("""
سیستم آماده است - کلید API تنظیم شده
""") else: gr.Markdown("""
⚠️ کلید API تنظیم نشده
لطفاً در Settings این Space، یک Secret با نام OPENROUTER_API_KEY اضافه کنید
برای دریافت کلید رایگان: https://openrouter.ai/keys
""") with gr.Row(): with gr.Column(scale=1): input_text = gr.Textbox( label="📝 متن ورودی", placeholder="متن مالی یا خبری خود را اینجا وارد کنید...", lines=12, max_lines=25 ) with gr.Row(): anonymize_btn = gr.Button( "🔒 ناشناس‌سازی", variant="primary", size="lg" ) clear_btn = gr.Button( "🗑️ پاک کردن", variant="secondary" ) with gr.Column(scale=1): output_text = gr.Textbox( label="🎯 متن ناشناس‌سازی شده", lines=12, max_lines=25, elem_classes=["result-box"] ) copy_btn = gr.Button( "📋 کپی متن", variant="secondary", size="sm" ) copy_output = gr.Textbox( label="📋 متن برای کپی (Ctrl+A و Ctrl+C)", lines=3, max_lines=10, visible=False, interactive=True ) with gr.Row(): with gr.Column(): statistics_output = gr.Markdown(label="📊 آمار کلی") with gr.Column(): quality_output = gr.Markdown(label="✅ کنترل کیفیت") with gr.Row(): with gr.Column(): entities_output = gr.Markdown(label="🏷️ موجودیت‌های شناسایی شده") with gr.Column(): detailed_analysis_output = gr.Markdown(label="🔍 تحلیل دقیق") usage_output = gr.Markdown(label="⚡ اطلاعات پردازش") def process_advanced_text(text: str): """پردازش پیشرفته متن""" if not text or not text.strip(): return ( "", "❌ لطفاً متن ورودی را وارد کنید", "", "", "", "" ) try: anonymizer = AdvancedOpenRouterAnonymizer() result = anonymizer.anonymize_text(text) if not result["success"]: return ( "", f"❌ خطا: {result['error']}", "", "", "", "" ) # آمار کلی stats = result.get("statistics", {}) stats_md = "📊 **آمار کلی:**\n\n" stats_md += f"""

🏢 شرکت‌ها

{stats.get('company', 0)}

👤 اشخاص

{stats.get('person', 0)}

💰 مبالغ

{stats.get('amount', 0)}

📊 درصدها

{stats.get('percent', 0)}

🔢 کل تغییرات

{stats.get('total_replacements', 0)}

""" # کنترل کیفیت quality = result.get("quality_check", {}) quality_md = "✅ **کنترل کیفیت:**\n\n" if quality.get("is_valid", False): quality_md += '✅ تمام بررسی‌ها موفق\n\n' else: quality_md += '❌ مشکلاتی یافت شد\n\n' issues = quality.get("issues", []) if issues: quality_md += "**مشکلات:**\n" for issue in issues: quality_md += f"• {issue}\n" # موجودیت‌ها entities = result.get("entities", {}) entities_md = "🏷️ **موجودیت‌های شناسایی شده:**\n\n" if entities.get("companies"): entities_md += f"🏢 **شرکت‌ها:** company-{', company-'.join(entities['companies'])}\n\n" if entities.get("persons"): entities_md += f"👤 **اشخاص:** person-{', person-'.join(entities['persons'])}\n\n" if entities.get("amounts"): entities_md += f"💰 **مبالغ:** amount-{', amount-'.join(entities['amounts'])}\n\n" if entities.get("percents"): entities_md += f"📊 **درصدها:** percent-{', percent-'.join(entities['percents'])}\n\n" # تحلیل دقیق detailed = result.get("detailed_analysis", {}) detailed_md = "🔍 **تحلیل دقیق:**\n\n" detailed_md += f"📅 **تاریخ‌های حفظ شده:** {detailed.get('preserved_dates', 0)}\n" detailed_md += f"🕐 **ساعت‌های حفظ شده:** {detailed.get('preserved_times', 0)}\n" detailed_md += f"📈 **شاخص‌های مالی:** {detailed.get('financial_indicators', 0)}\n" detailed_md += f"📏 **واحدهای حفظ شده:** {detailed.get('units_preserved', 0)}\n" # اطلاعات پردازش usage = result.get("usage", {}) usage_md = "⚡ **اطلاعات پردازش OpenRouter:**\n\n" if usage: usage_md += f"🤖 **مدل:** Qwen3-14B (14 میلیارد پارامتر)\n" usage_md += f"📥 **Token های ورودی:** {usage.get('prompt_tokens', 'نامشخص')}\n" usage_md += f"📤 **Token های خروجی:** {usage.get('completion_tokens', 'نامشخص')}\n" usage_md += f"📊 **کل Token ها:** {usage.get('total_tokens', 'نامشخص')}\n" usage_md += f"\n🆓 **مدل رایگان - بدون هزینه!**\n" usage_md += f"🎯 **دقت: 91% در تشخیص موجودیت فارسی**" else: usage_md += "✅ پردازش با موفقیت انجام شد" return ( result["anonymized_text"], stats_md, quality_md, entities_md, detailed_md, usage_md ) except Exception as e: return ( "", f"❌ خطایی غیرمنتظره: {str(e)}", "", "", "", "" ) def copy_text(text_to_copy): """تابع کپی متن""" if not text_to_copy or not text_to_copy.strip(): return gr.Textbox(visible=False), "⚠️ متنی برای کپی وجود ندارد" return gr.Textbox(value=text_to_copy, visible=True), "✅ متن در کادر زیر آماده کپی است" def clear_all(): """پاک کردن تمام فیلدها""" return "", "", "", "", "", "", "", gr.Textbox(visible=False) # اتصال رویدادها anonymize_btn.click( fn=process_advanced_text, inputs=[input_text], outputs=[output_text, statistics_output, quality_output, entities_output, detailed_analysis_output, usage_output] ) copy_btn.click( fn=copy_text, inputs=[output_text], outputs=[copy_output, statistics_output] ) clear_btn.click( fn=clear_all, outputs=[input_text, output_text, statistics_output, quality_output, entities_output, detailed_analysis_output, usage_output, copy_output] ) # مثال‌های پیشرفته gr.Examples( examples=[ ["مجمع عمومی عادی سالیانه شرکت پتروشیمی بوعلی سینا برگزار شد. شرکت وانیا نیک تدبیر را به‌عنوان بازرس قانونی انتخاب کردند. هزینه لجستیکی بوعلی حدود 100 میلیون دلار بوده است."], ["براساس آخرین گزارش سازمان تنظیم مقررات رادیویی در پاییز ۱۴۰۱ تعداد مشترکین تلفن همراه به بالای ۱۴۵ میلیون نفر رسیده است. همراه اول با سهمی ۵۳ درصدی بیشترین نقش را دارد."], ["شرکت فولاد مبارکه اصفهان با همکاری شرکت ملی نفت ایران، قرارداد توسعه میدان گازی را امضا کرد. شرکت فاما قصد دارد سرمایه خود را از ۸،۷۰۰ میلیارد ریال افزایش دهد."] ], inputs=input_text, label="📚 مثال‌های آزمایشی" ) # راهنما with gr.Accordion("📖 راهنمای استفاده", open=False): gr.Markdown(""" ## 🎯 نحوه استفاده: ### 1️⃣ تنظیم API Key (فقط یک بار): 1. برو به: https://openrouter.ai/keys 2. یک کلید رایگان بگیر 3. در Settings این Space، یک Secret اضافه کن: - **Name:** `OPENROUTER_API_KEY` - **Value:** کلید API خودت ### 2️⃣ استفاده: 1. متن خود را در کادر بالا وارد کنید 2. روی دکمه "ناشناس‌سازی" کلیک کنید 3. نتیجه را ببینید! ### 🏷️ انواع برچسب‌ها: - **company-XX:** شرکت‌ها، سازمان‌ها، گروه‌ها - **person-XX:** اشخاص - **amount-XX:** اعداد و مبالغ - **percent-XX:** درصدها ### 💡 نکات: - **مدل:** Qwen3-14B (14 میلیارد پارامتر) - **دقت:** 91% در تشخیص موجودیت فارسی - **محدودیت رایگان:** 200 درخواست در روز - **سرعت:** متوسط (10-15 ثانیه) - **Context:** تا 16K توکن ### 🚀 ویژگی‌های Qwen3-14B: - تشخیص هوشمند موجودیت‌های فارسی - درک context و ارجاعات implicit - حفظ ثبات در جایگزینی‌ها - پشتیبانی از متون مالی پیچیده """) return interface # اجرای برنامه if __name__ == "__main__": interface = create_advanced_interface() interface.launch()