leilaghomashchi commited on
Commit
344b293
·
verified ·
1 Parent(s): 35e30c2

Delete app.py

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