leilaghomashchi commited on
Commit
071b035
·
verified ·
1 Parent(s): 297803c

Upload app_rtl_fa (1).py

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