anonymization / app.py
leilaghomashchi's picture
Update app.py
ec40119 verified
"""
ناشناس‌ساز پیشرفته متون مالی/خبری فارسی
نسخه: 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()