File size: 25,977 Bytes
7e2b934
0d78fe8
ee6d2aa
7e2b934
 
 
 
0d78fe8
 
7e2b934
0d78fe8
 
7e2b934
0d78fe8
 
 
 
 
 
 
 
 
 
 
9112046
ee6d2aa
0d78fe8
ee6d2aa
 
ec40119
9112046
ee6d2aa
0d78fe8
 
 
 
7e2b934
0d78fe8
ee6d2aa
0d78fe8
 
 
 
ee6d2aa
 
0d78fe8
ee6d2aa
 
0d78fe8
2723c40
 
 
 
 
 
0d78fe8
2723c40
 
 
0d78fe8
2723c40
 
 
 
 
 
 
 
0d78fe8
2723c40
0d78fe8
2723c40
 
 
 
0d78fe8
2723c40
 
 
ee6d2aa
2723c40
 
 
 
 
 
ee6d2aa
2723c40
 
 
 
0d78fe8
2723c40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0d78fe8
 
7e2b934
0d78fe8
9112046
7e2b934
 
 
9112046
7e2b934
0d78fe8
ee6d2aa
7e2b934
0d78fe8
 
 
7e2b934
 
 
0d78fe8
 
 
 
 
 
7e2b934
9112046
0d78fe8
ee6d2aa
 
 
9112046
ee6d2aa
0d78fe8
 
7e2b934
0d78fe8
 
7e2b934
0d78fe8
ee6d2aa
0d78fe8
ee6d2aa
0d78fe8
ee6d2aa
0d78fe8
 
 
ee6d2aa
 
 
0d78fe8
ee6d2aa
 
 
 
 
 
 
0d78fe8
ee6d2aa
 
 
 
 
 
 
 
 
0d78fe8
ee6d2aa
0d78fe8
ee6d2aa
 
 
 
 
 
 
 
 
0d78fe8
7e2b934
0d78fe8
 
 
 
 
 
 
 
 
 
 
 
 
 
ee6d2aa
 
 
0d78fe8
 
 
 
 
 
 
 
ee6d2aa
0d78fe8
 
 
 
7e2b934
0d78fe8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee6d2aa
0d78fe8
 
ee6d2aa
0d78fe8
ee6d2aa
 
 
 
 
 
 
 
 
0d78fe8
 
 
9112046
0d78fe8
ee6d2aa
0d78fe8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9112046
0d78fe8
 
 
 
 
 
 
 
 
 
 
7e2b934
0d78fe8
 
ee6d2aa
0d78fe8
7e2b934
0d78fe8
 
 
9112046
ee6d2aa
 
 
 
0d78fe8
 
ee6d2aa
0d78fe8
9112046
ee6d2aa
0d78fe8
 
 
 
 
ee6d2aa
0d78fe8
 
 
 
 
 
ee6d2aa
 
 
 
 
0d78fe8
 
 
 
 
ee6d2aa
 
0d78fe8
 
 
 
 
 
 
ee6d2aa
0d78fe8
 
 
 
 
 
ee6d2aa
 
0d78fe8
 
 
 
 
 
ee6d2aa
 
0d78fe8
ee6d2aa
0d78fe8
ee6d2aa
0d78fe8
ee6d2aa
0d78fe8
ee6d2aa
0d78fe8
ee6d2aa
0d78fe8
 
 
 
ee6d2aa
 
 
 
 
 
0d78fe8
 
ee6d2aa
0d78fe8
ee6d2aa
0d78fe8
ee6d2aa
 
 
 
0d78fe8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee6d2aa
0d78fe8
 
 
 
 
ee6d2aa
0d78fe8
 
ee6d2aa
0d78fe8
 
 
ee6d2aa
0d78fe8
ee6d2aa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0d78fe8
ee6d2aa
 
0d78fe8
ee6d2aa
 
0d78fe8
ee6d2aa
 
0d78fe8
7e2b934
0d78fe8
7e2b934
 
0d78fe8
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
"""
ناشناس‌ساز پیشرفته متون مالی/خبری فارسی
نسخه: 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()