leilaghomashchi commited on
Commit
e451178
·
verified ·
1 Parent(s): cf99292

Upload app_rtl_fa_enhanced_FINAL.py

Browse files
Files changed (1) hide show
  1. app_rtl_fa_enhanced_FINAL.py +576 -0
app_rtl_fa_enhanced_FINAL.py ADDED
@@ -0,0 +1,576 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import re
3
+ import os
4
+ import requests
5
+ import logging
6
+ from typing import Dict, List, Tuple, Set
7
+ import json
8
+
9
+ logging.basicConfig(level=logging.INFO)
10
+ logger = logging.getLogger(__name__)
11
+
12
+ class AnonymizerCerebrasEnhanced:
13
+ def __init__(self, api_key: str = None):
14
+ self.api_key = api_key or os.getenv("CEREBRAS_API_KEY")
15
+ self.mapping_table = {}
16
+ self.counters = {
17
+ 'company': 0, 'person': 0, 'amount': 0, 'phone': 0,
18
+ 'email': 0, 'id_number': 0, 'date': 0, 'location': 0,
19
+ 'percent': 0
20
+ }
21
+ self.seen_entities = {} # برای ثبات نگاشت
22
+
23
+ if not self.api_key:
24
+ raise ValueError("❌ کلید API Cerebras یافت نشد!")
25
+
26
+ logger.info("✅ Anonymizer Enhanced مقداردهی شد")
27
+
28
+ def get_system_prompt(self) -> str:
29
+ """ایجاد دستورالعمل سیستمی پیشرفته برای Cerebras Llama 3.3-70B"""
30
+ return """شما یک «ناشناس‌ساز متون مالی فارسی پیشرفته» هستید. وظیفه‌تان:
31
+ 1. استخراج دقیق تمام موجودیت‌های حساس
32
+ 2. Entity Linking: ربط دادن نام‌های مختلف به یک موجودیت
33
+ 3. نگاشت ثابت تمام موجودیت‌ها
34
+ 4. ناشناس‌سازی دقیق و کامل
35
+
36
+ ## **📋 قوانین CRITICAL برای Entity Linking**
37
+
38
+ ### **1. ENTITY LINKING برای PERSONS (اشخاص)**
39
+
40
+ #### **قانون اول: نام کامل = معرف اصلی**
41
+ اگر "سروش خسروی" = person-01 شود:
42
+ - "سروش خسروی" → person-01 ✅
43
+ - "خسروی" تنهایی → person-01 ✅ (نام خانوادگی)
44
+ - "سروش" تنهایی → person-01 ✅ (نام کوچک)
45
+ - "او" / "وی" / "ایشان" → person-01 ✅ (ضمیر)
46
+ - "مهندس خسروی" → person-01 ✅ (عنوان + نام خانوادگی)
47
+
48
+ #### **قانون دوم: هیچ نام خانوادگی/کوچک تنهایی را رها نکن**
49
+ ✅ صحیح: "سروش خسروی در برنامه. خسروی گفت..." → هر دو را person-01
50
+ ❌ غلط: فقط خسروی را ناشناس کردن
51
+
52
+ #### **قانون سوم: الگوهای شامل نام**
53
+ اگر "سروش خسروی" = person-01، همه اینها = person-01:
54
+ - "سروش خسروی"
55
+ - "خسروی"
56
+ - "سروش"
57
+ - "مهندس خسروی"
58
+ - "رئیس خسروی"
59
+ - "وی" / "او" / "ایشان"
60
+
61
+ #### **قانون چهارم: ترتیب اولویت**
62
+ 1. نام کامل: "سروش خسروی" → person-01
63
+ 2. نام خانوادگی: "خسروی" → person-01
64
+ 3. نام کوچک: "سروش" → person-01
65
+ 4. عنوان + نام: "مهندس خسروی" → person-01
66
+ 5. ضمیرهای اشاره: "وی" → person-01
67
+
68
+ ### **2. ENTITY LINKING برای COMPANIES (شرکت‌ها)**
69
+
70
+ #### **قانون اول: نام کامل = معرف اصلی**
71
+ اگر "پتروشیمی غدیر (ماهشهر)" = company-01 شود:
72
+ - "پتروشیمی غدیر (ماهشهر)" → company-01 ✅
73
+ - "پتروشیمی غدیر" → company-01 ✅
74
+ - "غدیر" → company-01 ✅
75
+ - "این شرکت" (در فقره بعد) → company-01 ✅
76
+ - "شرکت" (در زمینه روشن) → company-01 ✅
77
+
78
+ #### **قانون دوم: نام‌های اختصاری**
79
+ اگر "نماد سبهان" = company-02:
80
+ - "نماد سبهان" → نماد company-02 ✅
81
+ - "سبهان" → company-02 ✅
82
+ - "نماد" تنهایی (در متن روشن) → company-02 ✅
83
+
84
+ ### **3. ENTITY LINKING برای AMOUNTS (مقادیر مالی)**
85
+
86
+ #### **قانون اول: واحد یکسان = یک موجودیت**
87
+ ✅ صحیح: "142 میلیارد تومان" → amount-01 | "142" دوباره → amount-01
88
+ ❌ غلط: "142" دوباره → amount-02 (NEW - غلط!)
89
+
90
+ #### **قانون دوم: واحد متفاوت = موجودیت مختلف**
91
+ "142 میلیارد تومان" → amount-01
92
+ "153,194 تن" → amount-02 (واحد متفاوت است)
93
+
94
+ #### **قانون سوم: فرمت‌های مختلف عدد یکسان**
95
+ ✅ صحیح: "142 میلیارد" → amount-01 | "۱۴۲ میلیارد" → amount-01
96
+ ❌ غلط: دو موجودیت متفاوت برای یک عدد
97
+
98
+ ### **4. ENTITY LINKING برای PERCENTS (درصدها)**
99
+
100
+ #### **قانون اول: مقدار و واحد یکسان**
101
+ "21 درصد" → percent-01
102
+ "21%" یا "۲۱ درصدی" → percent-01 ✅
103
+ "45 درصد" → percent-02 ✅ (متفاوت است)
104
+
105
+ #### **قانون دوم: درصد در سیاق‌های مختلف**
106
+ "رشد 21 درصدی" → percent-01
107
+ "افزایش 21 درصد" → percent-01 ✅ (همان درصد)
108
+
109
+ ## **🔍 الگوریتم STEP-BY-STEP**
110
+
111
+ ### **مرحله 1: استخراج نخستین**
112
+ تمام موجودیت‌های کامل را بدون linked کردن استخراج کن
113
+
114
+ ### **مرحله 2: Entity Linking**
115
+ برای هر موجودیت تام:
116
+ - نام خانوادگی استخراج کن (اگر موجود باشد)
117
+ - نام کوچک استخراج کن (اگر موجود باشد)
118
+ - در تمام متن جستجو کن
119
+ - تمام تکرارها = یک موجودیت
120
+
121
+ ### **مرحله 3: نگاشت نهایی**
122
+ برای هر موجودیت اصلی، تمام linked forms را ثبت کن
123
+
124
+ ### **مرحله 4: ناشناس‌سازی**
125
+ جایگزین کن:
126
+ 1. نام‌های کامل اول
127
+ 2. سپس نام‌های جزئی
128
+ 3. سپس ضمیرهایی که الحاق شده‌اند
129
+
130
+ ## **✅ CRITICAL DO's:**
131
+ ✅ "خسروی" بعد از "سروش خسروی" → person-01
132
+ ✅ "غدیر" بعد از "پتروشیمی غدیر" → company-01
133
+ ✅ "این شرکت" در فقره بعد از ذکر شرکت → company-01
134
+ ✅ "وی" که اشاره به شخص → person-01
135
+ ✅ تمام تکرار نام خانوادگی → همان person
136
+ ✅ نام کامل مختلف → person متفاوت
137
+
138
+ ## **❌ CRITICAL DON'Ts:**
139
+ ❌ نام خانوادگی تنهایی را رها کردن
140
+ ❌ نام کوچک تنهایی را رها کردن
141
+ ❌ ضمیرهای مرتبط را رها کردن
142
+ ❌ فرض کردن اینکه "خسروی" مختلف است
143
+ ❌ نادیده گرفتن زمینه برای "این شرکت"
144
+ ❌ دو بار شماره‌گذاری موجودیت یکسان
145
+
146
+ ## **⚠️ قوانین اندیس‌گذاری:**
147
+ - شرکت‌ها: company-01, company-02, company-03, ... (پیوسته و بدون گپ)
148
+ - اشخاص: person-01, person-02, person-03, ... (پیوسته و بدون گپ)
149
+ - مقادیر: amount-01, amount-02, amount-03, ... (پیوسته و بدون گپ)
150
+ - درصدها: percent-01, percent-02, percent-03, ... (پیوسته و بدون گپ)
151
+ - تاریخها: date-01, date-02, date-03, ... (پیوسته و بدون گپ)
152
+
153
+ ## **موارد حفظ شده:**
154
+ - عناوین شغلی: مدیرعامل، رئیس کل، مدیرکل، سرپرست
155
+ - واحدها: میلیارد تومان، هزار ریال، دلار، تن، دستگاه
156
+ - کلمات توضیحی: "شرکت"، "بانک"، "گروه"، "سازمان"
157
+
158
+ ## **ممنوع:**
159
+ ❌ کلمات انگلیسی اضافی
160
+ ❌ تغییر ساختار جمله
161
+ ❌ حذف یا اضافه کردن کلمات
162
+ ❌ نام خانوادگی یا نام کوچک تنهایی را بدون linking رها کردن
163
+
164
+ **فقط متن ناشناس‌شده را برگردان - هیچ توضیح اضافی نیاز نیست."""
165
+
166
+ def get_user_prompt(self, text: str) -> str:
167
+ """تشکیل پرامپت کاربر قوی‌تر برای Entity Linking دقیق"""
168
+ return f"""متن مالی فارسی زیر را با دقت تحلیل کنید و تمام موجودیت‌های حساس را ناشناس کنید.
169
+
170
+ متن:
171
+ {text}
172
+
173
+ **دستورات CRITICAL برای Entity Linking:**
174
+
175
+ ### **برای اشخاص (PERSONS):**
176
+ 1️⃣ اگر "سروش خسروی" = person-01 شد:
177
+ - نام خانوادگی تنهایی "خسروی" → person-01
178
+ - نام کوچک تنهایی "سروش" → person-01
179
+ - ضمیرهای "وی"، "او"، "ایشان" → person-01
180
+ - "مهندس خسروی" یا هر ترکیب → person-01
181
+
182
+ 2️⃣ هیچ نام خانوادگی یا نام کوچک تنهایی را رها نکن
183
+
184
+ ### **برای شرکت‌ها (COMPANIES):**
185
+ 1️⃣ اگر "پتروشیمی غدیر" = company-01 شد:
186
+ - نام کوتاه "غدیر" → company-01
187
+ - "این شرکت" در فقره بعد → company-01
188
+ - "شرکت" در زمینه روشن → company-01
189
+
190
+ 2️⃣ تمام نام‌های مختلف برای یک شرکت را linked کن
191
+
192
+ ### **برای مقادیر (AMOUNTS):**
193
+ 1️⃣ اگر "142 میلیارد تومان" = amount-01:
194
+ - "142 میلیارد" یا صرفاً "142" = amount-01
195
+ - "۱۴۲ میلیارد" = amount-01 (فرمت متفاوت، عدد یکسان)
196
+
197
+ 2️⃣ واحد متفاوت = موجودیت متفاوت
198
+
199
+ ### **برای درصدها (PERCENTS):**
200
+ 1️⃣ اگر "21 درصد" = percent-01:
201
+ - "21%" = percent-01
202
+ - "۲۱ درصدی" = percent-01
203
+ - "رشد 21 درصدی" = percent-01
204
+
205
+ **‌خروجی:**
206
+ ناشناس‌سازی متن با رعایت تمام قوانین Entity Linking فوق.
207
+
208
+ **مهم:**
209
+ ✅ تمام نام‌های جزئی (نام خانوادگی، نام کوچک) linked باشند
210
+ ✅ تمام ضمیرهای مرتبط linked باشند
211
+ ✅ تمام نام‌های مختلف یک شرکت linked باشند
212
+ ✅ مقادیر و درصدهای یکسان (با واحد/فرمت یکسان) = یک موجودیت
213
+
214
+ ❌ هیچ نام خانوادگی تنهایی رها نشود
215
+ ❌ هیچ ضمیر مرتبط رها نشود
216
+ ❌ هیچ نام اختصاری شرکت رها نشود
217
+
218
+ **فقط متن ناشناس‌شده را برگردان - توضیح اضافی نده!**"""
219
+
220
+ def call_cerebras(self, text: str) -> List[Dict]:
221
+ """فراخوانی Cerebras API با پرامپت بهبود شده"""
222
+ logger.info("🔄 فراخوانی Cerebras API با دستورالعمل قوی...")
223
+
224
+ system_prompt = self.get_system_prompt()
225
+ user_prompt = self.get_user_prompt(text)
226
+
227
+ try:
228
+ response = requests.post(
229
+ "https://api.cerebras.ai/v1/chat/completions",
230
+ headers={
231
+ "Authorization": f"Bearer {self.api_key}",
232
+ "Content-Type": "application/json"
233
+ },
234
+ json={
235
+ "model": "llama-3.3-70b",
236
+ "messages": [
237
+ {"role": "system", "content": system_prompt},
238
+ {"role": "user", "content": user_prompt}
239
+ ],
240
+ "max_tokens": 4000,
241
+ "temperature": 0.1
242
+ },
243
+ timeout=30
244
+ )
245
+
246
+ if response.status_code != 200:
247
+ logger.error(f"❌ خطای API Cerebras: {response.text}")
248
+ return []
249
+
250
+ result = response.json()
251
+ content = result['choices'][0]['message']['content']
252
+
253
+ try:
254
+ # تمیز کردن محتوا از markdown اگر وجود داشته باشد
255
+ content = content.replace("```json", "").replace("```", "").strip()
256
+ entities = json.loads(content)
257
+ if not isinstance(entities, list):
258
+ entities = []
259
+ logger.info(f"✅ {len(entities)} موجودیت استخراج شد")
260
+ return entities
261
+ except json.JSONDecodeError:
262
+ logger.error(f"❌ خطا در JSON parsing: {content[:200]}")
263
+ return []
264
+
265
+ except Exception as e:
266
+ logger.error(f"❌ خطا Cerebras: {e}")
267
+ return []
268
+
269
+ def get_placeholder(self, entity_type: str) -> str:
270
+ """تولید placeholder با format جدید"""
271
+ type_lower = entity_type.lower()
272
+ if type_lower not in self.counters:
273
+ type_lower = 'amount'
274
+
275
+ self.counters[type_lower] += 1
276
+ return f"{type_lower}-{self.counters[type_lower]:02d}"
277
+
278
+ def anonymize(self, text: str) -> Tuple[str, List]:
279
+ """ناشناس‌سازی متن با قوانین ثبات"""
280
+ logger.info("🚀 شروع ناشناس‌سازی متن...")
281
+
282
+ # تنظیف
283
+ self.mapping_table = {}
284
+ self.seen_entities = {}
285
+ for key in self.counters:
286
+ self.counters[key] = 0
287
+
288
+ # دریافت موجودیت‌ها
289
+ entities = self.call_cerebras(text)
290
+
291
+ if not entities:
292
+ logger.warning("⚠️ موجودیتی شناسایی نشد")
293
+ return text, []
294
+
295
+ logger.info("🔄 Processing entities...")
296
+
297
+ # جایگزینی با قانون ثبات
298
+ anonymized = text
299
+ replacements = []
300
+
301
+ for entity in entities:
302
+ entity_type = entity.get('type', 'amount').lower()
303
+ entity_text = entity.get('text', '').strip()
304
+ original_info = entity.get('original', '')
305
+
306
+ if not entity_text:
307
+ continue
308
+
309
+ # بررسی اگر این موجودیت قبلاً دیده شده است
310
+ entity_key = (entity_type, entity_text.lower())
311
+
312
+ if entity_key in self.seen_entities:
313
+ token = self.seen_entities[entity_key]
314
+ logger.info(f"🔄 موجودیت تکراری: {entity_text} → {token}")
315
+ else:
316
+ token = self.get_placeholder(entity_type)
317
+ self.seen_entities[entity_key] = token
318
+ self.mapping_table[token] = {
319
+ 'original': entity_text,
320
+ 'type': entity_type,
321
+ 'note': original_info
322
+ }
323
+ logger.info(f"✅ جایگزینی: {entity_text} → {token}")
324
+
325
+ # جایگزینی دقیق (case-sensitive اول، سپس case-insensitive)
326
+ idx = anonymized.find(entity_text)
327
+ if idx != -1:
328
+ anonymized = anonymized[:idx] + token + anonymized[idx + len(entity_text):]
329
+ replacements.append({
330
+ 'original': entity_text,
331
+ 'placeholder': token,
332
+ 'type': entity_type
333
+ })
334
+
335
+ logger.info(f"✅ ناشناس‌سازی کامل - {len(self.mapping_table)} نگاشت")
336
+ return anonymized, entities
337
+
338
+ def get_mapping_table_str(self) -> str:
339
+ """جدول نگاشت جزئی"""
340
+ if not self.mapping_table:
341
+ return "❌ موجودیتی شناسایی نشد"
342
+
343
+ result = "## 📊 جدول نگاشت\n\n"
344
+ result += "| توکن | اطلاعات اصلی | نوع |\n"
345
+ result += "|------|--------|------|\n"
346
+
347
+ for token, info in sorted(self.mapping_table.items()):
348
+ entity_type = info.get('type', 'unknown')
349
+ original = info.get('original', '')
350
+ note = info.get('note', '')
351
+
352
+ note_str = f" ({note})" if note else ""
353
+ result += f"| `{token}` | {original}{note_str} | {entity_type} |\n"
354
+
355
+ return result
356
+
357
+ def restore(self, text: str) -> str:
358
+ """بازگردانی اطلاعات اصلی"""
359
+ logger.info("🔄 بازگردانی اطلاعات...")
360
+ restored = text
361
+ for token, info in self.mapping_table.items():
362
+ original = info.get('original', '')
363
+ restored = restored.replace(token, original)
364
+ logger.info("✅ بازگردانی کامل")
365
+ return restored
366
+
367
+
368
+ # متغیرهای global
369
+ anonymizer = None
370
+
371
+ def process(input_text: str) -> Tuple[str, str, str, str, str]:
372
+ """
373
+ روند کامل:
374
+ 1. ناشناس‌سازی با Cerebras (llama-3.3-70b) + پرامپت قوی
375
+ 2. ارسال به ChatGPT (حتما!)
376
+ 3. بازگردانی پاسخ ChatGPT
377
+ """
378
+ global anonymizer
379
+
380
+ try:
381
+ if not input_text.strip():
382
+ return "", "", "", "", ""
383
+
384
+ # دریافت API Keys
385
+ api_key_cerebras = os.getenv("CEREBRAS_API_KEY")
386
+ api_key_gpt = os.getenv("OPENAI_API_KEY")
387
+
388
+ if not api_key_gpt:
389
+ logger.error("❌ OPENAI_API_KEY یافت نشد")
390
+ return "", "", "", "", ""
391
+
392
+ if not api_key_cerebras:
393
+ logger.error("❌ CEREBRAS_API_KEY یافت نشد")
394
+ return "", "", "", "", ""
395
+
396
+ # ============================================
397
+ # مرحله 1: مقداردهی
398
+ # ============================================
399
+ if not anonymizer:
400
+ logger.info("Initializing anonymizer...")
401
+ anonymizer = AnonymizerCerebrasEnhanced()
402
+
403
+ # ============================================
404
+ # مرحله 2: ناشناس‌سازی با پرامپت قوی
405
+ # ============================================
406
+ logger.info("Step 1: Anonymizing text with Cerebras...")
407
+
408
+ anonymized_text, entities = anonymizer.anonymize(input_text)
409
+
410
+ if not entities:
411
+ logger.warning("⚠️ موجودیتی شناسایی نشد - متن ناشناس نشد")
412
+ return input_text, "", "", "", ""
413
+
414
+ # ============================================
415
+ # مرحله 3: جدول نگاشت
416
+ # ============================================
417
+ logger.info("Step 2: Creating mapping table")
418
+ mapping = anonymizer.get_mapping_table_str()
419
+ logger.info(f"📋 {len(anonymizer.mapping_table)} نگاشت ایجاد شد")
420
+
421
+ # ============================================
422
+ # مرحله 4: ارسال به ChatGPT (حتما!)
423
+ # ============================================
424
+ logger.info("Step 3: Sending to ChatGPT...")
425
+
426
+ prompt = f"""متن ناشناس‌شده زیر (متن مالی) را تحلیل و خلاصه کنید.
427
+
428
+ متن:
429
+ {anonymized_text}
430
+
431
+ لطفاً:
432
+ 1. خلاصه‌ای مختصر و معنادار ارائه دهید
433
+ 2. نکات اصلی را مشخص کنید
434
+ 3. تمام توکن‌های ناشناس (مثل company-01، amount-02) را حفظ کنید
435
+ 4. تنها اطلاعات موجود در متن را بیان کنید"""
436
+
437
+ logger.info(f"📤 ارسال به ChatGPT (gpt-4o-mini)...")
438
+
439
+ try:
440
+ gpt_response_obj = requests.post(
441
+ "https://api.openai.com/v1/chat/completions",
442
+ headers={"Authorization": f"Bearer {api_key_gpt}"},
443
+ json={
444
+ "model": "gpt-4o-mini",
445
+ "messages": [
446
+ {
447
+ "role": "system",
448
+ "content": "شما دستیار تحلیل متون مالی فارسی هستید. متن‌های ناشناس‌شده را دقیق تحلیل کنید. تمام توکن‌های ناشناس را حفظ کنید."
449
+ },
450
+ {"role": "user", "content": prompt}
451
+ ],
452
+ "max_tokens": 1500,
453
+ "temperature": 0.7
454
+ },
455
+ timeout=30
456
+ )
457
+
458
+ if gpt_response_obj.status_code == 200:
459
+ gpt_response = gpt_response_obj.json()['choices'][0]['message']['content']
460
+ logger.info("✅ پاسخ دریافت شد")
461
+ else:
462
+ error_text = gpt_response_obj.json().get('error', {}).get('message', gpt_response_obj.text)
463
+ logger.error(f"❌ خطای ChatGPT: {error_text}")
464
+ return input_text, anonymized_text, "", "", mapping
465
+
466
+ except Exception as e:
467
+ logger.error(f"❌ خطا در ارسال به ChatGPT: {e}")
468
+ return input_text, anonymized_text, "", "", mapping
469
+
470
+ # ============================================
471
+ # مرحله 5: بازگردانی پاسخ ChatGPT
472
+ # ============================================
473
+ logger.info("Step 4: Restoring original text...")
474
+
475
+ restored_text = anonymizer.restore(gpt_response)
476
+
477
+ logger.info(f"✅ بازگردانی کامل")
478
+
479
+ logger.info(f"Done. Input: {len(input_text)} | Anonymized: {len(anonymized_text)} | Entities: {len(entities)}")
480
+
481
+ return input_text, anonymized_text, gpt_response, restored_text, mapping
482
+
483
+ except Exception as e:
484
+ logger.error(f"❌ خطا عمومی: {e}", exc_info=True)
485
+ return "", "", "", "", ""
486
+
487
+ def clear():
488
+ """پاک کردن"""
489
+ empty_mapping = "### 📋 جدول نگاشت\nدر انتظار پردازش..."
490
+ return "", "", "", "", empty_mapping
491
+
492
+ # رابط Gradio - کاملاً فارسی‌زبان و RTL
493
+ css_rtl = """
494
+ #input_text textarea { direction: rtl; text-align: right; }
495
+ #anonymized_text textarea { direction: rtl; text-align: right; }
496
+ #gpt_response textarea { direction: rtl; text-align: right; }
497
+ #restored_text textarea { direction: rtl; text-align: right; }
498
+ """
499
+
500
+ with gr.Blocks(title="سیستم ناشناس‌سازی متون", theme=gr.themes.Soft(), css=css_rtl) as app:
501
+
502
+ gr.Markdown("# 🔐 سیستم ناشناس‌سازی متون مالی فارسی")
503
+ gr.Markdown("#### استخراج موجودیت‌های حساس و ناشناس‌سازی آنها")
504
+
505
+ with gr.Row():
506
+ # بلوک 1: متن ورودی (سمت راست)
507
+ with gr.Column(scale=2):
508
+ input_text = gr.Textbox(
509
+ lines=12,
510
+ placeholder="متن مالی/خبری را وارد کنید...",
511
+ label="📝 متن ورودی",
512
+ elem_id="input_text"
513
+ )
514
+
515
+ # دکمه‌های کنترل
516
+ with gr.Column(scale=1):
517
+ gr.HTML("<div style='text-align: center; margin-bottom: 10px;'></div>")
518
+ process_btn = gr.Button("🔄 پردازش", variant="primary", size="lg")
519
+ clear_btn = gr.Button("🗑️ پاک کردن", variant="stop", size="lg")
520
+
521
+ # بلوک 2: متن ناشناس‌سازی شده
522
+ with gr.Row():
523
+ with gr.Column(scale=1):
524
+ anonymized_text = gr.Textbox(
525
+ lines=10,
526
+ label="🔒 متن ناشناس‌شده",
527
+ interactive=False,
528
+ elem_id="anonymized_text"
529
+ )
530
+
531
+ # بلوک 3: پاسخ ChatGPT
532
+ with gr.Column(scale=1):
533
+ gpt_response = gr.Textbox(
534
+ lines=10,
535
+ label="🤖 تحلیل ChatGPT",
536
+ interactive=False,
537
+ elem_id="gpt_response"
538
+ )
539
+
540
+ # بلوک 4: متن بازگردانی شده (سمت چپ)
541
+ with gr.Column(scale=1):
542
+ restored_text = gr.Textbox(
543
+ lines=10,
544
+ label="✅ متن بازگردانی شده",
545
+ interactive=False,
546
+ elem_id="restored_text"
547
+ )
548
+
549
+ # بلوک 5: جدول نگاشت به صورت مارکداون
550
+ with gr.Row():
551
+ with gr.Column():
552
+ mapping = gr.Markdown(
553
+ value="### 📋 جدول نگاشت\nدر انتظار پردازش...",
554
+ label="📋 جدول نگاشت"
555
+ )
556
+
557
+ # Event handlers
558
+ process_btn.click(
559
+ fn=process,
560
+ inputs=[input_text],
561
+ outputs=[input_text, anonymized_text, gpt_response, restored_text, mapping]
562
+ )
563
+
564
+ clear_btn.click(
565
+ fn=clear,
566
+ outputs=[input_text, anonymized_text, gpt_response, restored_text, mapping]
567
+ )
568
+
569
+ if __name__ == "__main__":
570
+ print("🚀 سیستم ناشناس‌سازی متون در حال راه‌اندازی...")
571
+ app.launch(
572
+ server_name="0.0.0.0",
573
+ server_port=7860,
574
+ share=False,
575
+ show_error=True
576
+ )