KashefTech commited on
Commit
b8a6e5b
·
verified ·
1 Parent(s): 7e8829f

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -765
app.py DELETED
@@ -1,765 +0,0 @@
1
- import gradio as gr
2
- import re
3
- import os
4
- import requests
5
- import json
6
- import logging
7
- from typing import Dict, List, Tuple, Optional
8
- from llm_sender_unified import create_llm_sender
9
-
10
- logging.basicConfig(level=logging.INFO)
11
- logger = logging.getLogger(__name__)
12
-
13
- # ─────────────────────────────────────────────────────────────
14
- # مدل‌های موجود
15
- # ─────────────────────────────────────────────────────────────
16
- AVAILABLE_MODELS = {
17
- "chatgpt": [
18
- "gpt-5.1",
19
- "gpt-5",
20
- "gpt-4.1",
21
- "gpt-4o",
22
- "gpt-4o-mini",
23
- "gpt-4-turbo",
24
- ],
25
- "grok": [
26
- "grok-4-0709",
27
- "grok-3",
28
- "grok-3-mini",
29
- "grok-2-1212",
30
- ],
31
- "deepinfra": [
32
- "Qwen/Qwen3-14B",
33
- "Qwen/Qwen3-32B",
34
- "Qwen/Qwen3-30B-A3B",
35
- "Qwen/Qwen2.5-72B-Instruct",
36
- "Qwen/Qwen2.5-14B-Instruct",
37
- ]
38
- }
39
-
40
- # ─────────────────────────────────────────────────────────────
41
- # SYSTEM PROMPT — فشرده، رفع باگ bank-XX و شرکت-01
42
- # ─────────────────────────────────────────────────────────────
43
- DEEPINFRA_SYSTEM_PROMPT = """You are a Persian text anonymizer. Output ONLY anonymized text with NO explanation.
44
-
45
- TOKENS: company-01/02/03... | person-01/02/03... | amount-01/02/03... | percent-01/02/03...
46
-
47
- RULES:
48
- - ALL organizations → company-XX (banks, funds, auditors, any named entity)
49
- - NEVER: bank-01, org-01, شرکت-01, بانک-01 (only company-XX in English)
50
- - Same entity = same token. New entity = new token. Sequential numbering.
51
- - Keep non-sensitive words exactly as-is."""
52
-
53
-
54
- # ─────────────────────────────────────────────────────────────
55
- # توابع ساخت prompt
56
- # ─────────────────────────────────────────────────────────────
57
-
58
- def build_anonymization_prompt(text: str, entities: list) -> str:
59
- """
60
- ساخت prompt ناشناس‌سازی — dynamic و فشرده
61
- باگ‌های رفع‌شده:
62
- - نام‌های بدون پیشوند (ایران خودرو، تیپیکو)
63
- - bank-XX / شرکت-01
64
- - اعداد خالص (P/E=4، 3.2 واحد)
65
- - درصد range → دو توکن جداگانه
66
- - واحد پولی در متن حفظ می‌شود
67
- """
68
- lines = []
69
-
70
- if "company" in entities:
71
- lines.append(
72
- "company-XX → ALL org names (شرکت/بانک/صندوق/هلدینگ/حسابرس)\n"
73
- " • نام‌های بدون پیشوند هم ناشناس شوند: ایران خودرو→company-01, تیپیکو→company-02"
74
- )
75
-
76
- if "person" in entities:
77
- lines.append("person-XX → نام و نام‌خانوادگی اشخاص حقیقی")
78
-
79
- if "amount" in entities:
80
- lines.append(
81
- "amount-XX → اعداد/مبالغ (واحد را در متن نگه‌دار، فقط عدد را جایگزین کن)\n"
82
- " • «23 هزار و 296 میلیارد تومان» → amount-01 میلیارد تومان\n"
83
- " • «537 هزار و 736 دستگاه» → amount-02 هزار و 736 دستگاه\n"
84
- " • «P/E به 4 رسید» → P/E به amount-03 رسید\n"
85
- " • «3.2 واحد» → amount-04 واحد"
86
- )
87
-
88
- if "percent" in entities:
89
- lines.append(
90
- "percent-XX → درصدها (هر عدد = یک توکن مجزا)\n"
91
- " • «4.58 درصد» → percent-01 درصد\n"
92
- " • «50 الی 70 درصد» → percent-02 الی percent-03 درصد ← دو توکن\n"
93
- " • «40–60٪» → percent-04–percent-05 ← دو توکن\n"
94
- " • «منفی 345 درصد» → منفی percent-06 درصد"
95
- )
96
-
97
- lines.append("اعداد سال (1402، 1403) و اعداد ترتیبی (12 بانک، سه شرکت) را ناشناس نکن")
98
-
99
- rules = "\n".join(lines)
100
-
101
- return f"""متن زیر را طبق قوانین ناشناس کن. فقط متن ناشناس شده را بده، بدون توضیح.
102
-
103
- {rules}
104
-
105
- متن:
106
- {text}"""
107
-
108
-
109
- def build_mapping_prompt(original: str, anonymized: str, entities: list) -> str:
110
- """ساخت prompt mapping — فشرده"""
111
- type_hints = []
112
- if "person" in entities: type_hints.append('person-XX: "نام کامل"')
113
- if "company" in entities: type_hints.append('company-XX: "نام کامل سازمان"')
114
- if "amount" in entities: type_hints.append('amount-XX: "عدد + واحد کامل مثل 23 هزار میلیارد تومان"')
115
- if "percent" in entities: type_hints.append('percent-XX: "عدد + درصد مثل 4.58 درصد"')
116
-
117
- hints = " | ".join(type_hints)
118
-
119
- return f"""متن اصلی: {original}
120
- متن ناشناس: {anonymized}
121
-
122
- JSON mapping (فقط JSON، بدون توضیح):
123
- فرمت: {{ "token": "مقدار_اصلی" }}
124
- راهنما: {hints}"""
125
-
126
-
127
- def build_analysis_prompt(anonymized_text: str, analysis_prompt: str, entities: list) -> str:
128
- """ساخت prompt تحلیل LLM — یکپارچه برای همه مدل‌ها"""
129
- tokens = []
130
- if "person" in entities: tokens.append("person-XX")
131
- if "company" in entities: tokens.append("company-XX")
132
- if "amount" in entities: tokens.append("amount-XX")
133
- if "percent" in entities: tokens.append("percent-XX")
134
- tokens_str = ", ".join(tokens)
135
-
136
- return f"""متن ناشناس‌سازی شده:
137
- {anonymized_text}
138
-
139
- دستورات:
140
- {analysis_prompt}
141
-
142
- قوانین:
143
- - فقط از توکن‌های موجود استفاده کن: {tokens_str}
144
- - هیچ کلمه‌ای قبل/بعد از توکن‌ها اضافه نکن
145
- - توکن جدید ایجاد نکن"""
146
-
147
-
148
- # ─────────────────────────────────────────────────────────────
149
- # تابع کمکی: حذف بلوک‌های think از Qwen3
150
- # ─────────────────────────────────────────────────────────────
151
- def strip_thinking(text: str) -> str:
152
- if not text:
153
- return text
154
- cleaned = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
155
- return cleaned.strip()
156
-
157
-
158
- # ─────────────────────────────────────────────────────────────
159
- # کلاس اصلی
160
- # ─────────────────────────────────────────────────────────────
161
- class AnonymizerAdvanced:
162
-
163
- def __init__(
164
- self,
165
- deepinfra_key: str = None,
166
- llm_provider: str = "chatgpt",
167
- llm_model: str = None,
168
- entities_to_anonymize: List[str] = None
169
- ):
170
- self.deepinfra_key = deepinfra_key or os.getenv("DEEPINFRA_API_KEY")
171
- self.llm_provider = llm_provider
172
- self.llm_model = llm_model
173
- self.entities_to_anonymize = entities_to_anonymize or ["person", "company", "amount", "percent"]
174
- self.mapping_table: Dict[str, str] = {}
175
- self.reverse_mapping: Dict[str, str] = {}
176
- self._create_llm_sender()
177
- logger.info(f"✅ Anonymizer مقداردهی شد — {llm_provider}")
178
-
179
- # ── LLM sender ──────────────────────────────────────────
180
-
181
- def _create_llm_sender(self):
182
- try:
183
- key_map = {
184
- "chatgpt": os.getenv("OPENAI_API_KEY"),
185
- "grok": os.getenv("XAI_API_KEY"),
186
- "deepinfra": os.getenv("DEEPINFRA_API_KEY"),
187
- }
188
- api_key = key_map.get(self.llm_provider)
189
- self.llm_sender = create_llm_sender(
190
- provider=self.llm_provider,
191
- api_key=api_key,
192
- model=self.llm_model
193
- )
194
- logger.info(f"✅ LLM Sender: {self.llm_provider} — {self.llm_sender.model}")
195
- except Exception as e:
196
- logger.error(f"❌ خطا در ایجاد LLM Sender: {e}")
197
- self.llm_sender = create_llm_sender("chatgpt")
198
-
199
- def set_llm_provider(self, provider: str, model: str = None, entities: List[str] = None):
200
- self.llm_provider = provider
201
- self.llm_model = model
202
- if entities is not None:
203
- self.entities_to_anonymize = entities
204
- self._create_llm_sender()
205
- logger.info(f"✅ LLM تغییر یافت: {provider} — {model}")
206
-
207
- # ── ناشناس‌سازی با DeepInfra ──────────────────────────
208
-
209
- def anonymize_with_deepinfra(self, text: str) -> Tuple[str, Dict]:
210
- logger.info("🧠 ناشناس‌سازی با DeepInfra...")
211
-
212
- if not self.deepinfra_key:
213
- raise ValueError("DeepInfra API Key مورد نیاز است")
214
-
215
- if not self.entities_to_anonymize:
216
- logger.warning("⚠️ هیچ موجودیتی انتخاب نشده")
217
- return text, {}
218
-
219
- headers = {
220
- "Authorization": f"Bearer {self.deepinfra_key}",
221
- "Content-Type": "application/json"
222
- }
223
-
224
- try:
225
- # ── مرحله ۱: ��اشناس‌سازی ────────────────────────
226
- prompt1 = build_anonymization_prompt(text, self.entities_to_anonymize)
227
-
228
- resp1 = requests.post(
229
- "https://api.deepinfra.com/v1/openai/chat/completions",
230
- headers=headers,
231
- json={
232
- "model": "Qwen/Qwen3-14B",
233
- "messages": [
234
- {"role": "system", "content": DEEPINFRA_SYSTEM_PROMPT},
235
- {"role": "user", "content": prompt1}
236
- ],
237
- "max_tokens": 4096,
238
- "temperature": 0.1
239
- },
240
- timeout=90
241
- )
242
-
243
- if resp1.status_code != 200:
244
- raise Exception(f"DeepInfra API Error: {resp1.status_code} — {resp1.text[:200]}")
245
-
246
- anonymized_text = strip_thinking(
247
- resp1.json()["choices"][0]["message"]["content"]
248
- )
249
- logger.info("✅ ناشناس‌سازی موفق")
250
-
251
- # debug: توکن‌های موجود
252
- for etype in self.entities_to_anonymize:
253
- found = sorted(set(re.findall(rf'{etype}-\d+', anonymized_text)))
254
- logger.info(f" {etype}: {found}")
255
-
256
- # ── مرحله ۲: استخراج mapping ────────────────────
257
- prompt2 = build_mapping_prompt(text, anonymized_text, self.entities_to_anonymize)
258
-
259
- resp2 = requests.post(
260
- "https://api.deepinfra.com/v1/openai/chat/completions",
261
- headers=headers,
262
- json={
263
- "model": "Qwen/Qwen3-14B",
264
- "messages": [
265
- {"role": "system", "content": DEEPINFRA_SYSTEM_PROMPT},
266
- {"role": "user", "content": prompt2}
267
- ],
268
- "max_tokens": 2048,
269
- "temperature": 0.1
270
- },
271
- timeout=60
272
- )
273
-
274
- if resp2.status_code == 200:
275
- raw = strip_thinking(resp2.json()["choices"][0]["message"]["content"])
276
- # پاک‌سازی fence های markdown
277
- raw = re.sub(r"```(?:json)?", "", raw).replace("```", "").strip()
278
- try:
279
- self.mapping_table = json.loads(raw)
280
- self._fix_mapping()
281
- self.reverse_mapping = {v: k for k, v in self.mapping_table.items()}
282
- logger.info(f"✅ Mapping: {len(self.mapping_table)} موجودیت")
283
- except json.JSONDecodeError:
284
- logger.warning("⚠️ JSON mapping خطا — fallback")
285
- self._extract_mapping_fallback(text, anonymized_text)
286
- else:
287
- logger.warning("⚠️ mapping API خطا — fallback")
288
- self._extract_mapping_fallback(text, anonymized_text)
289
-
290
- return anonymized_text, self.mapping_table
291
-
292
- except Exception as e:
293
- logger.error(f"❌ DeepInfra Exception: {e}")
294
- raise
295
-
296
- # ── اصلاح mapping ────────────────────────────────────────
297
-
298
- def _fix_mapping(self):
299
- """اطمینان از کامل بودن مقادیر percent در mapping"""
300
- for token, value in list(self.mapping_table.items()):
301
- val = str(value).strip()
302
- if token.startswith("percent-") and not re.search(r"(درصد|%|درصدی)", val):
303
- self.mapping_table[token] = f"{val} درصد"
304
- logger.info(f" اصلاح {token}: '{val}' → '{val} درصد'")
305
-
306
- # ── fallback mapping با regex ────────────────────────────
307
-
308
- def _extract_mapping_fallback(self, original: str, anonymized: str):
309
- """
310
- استخراج mapping با regex — وقتی JSON mapping شکست خورد
311
- باگ رفع‌شده: \b برای فارسی کار نمی‌کند → از lookahead/lookbehind استفاده شد
312
- """
313
- # الگوهای موجودیت
314
- patterns: Dict[str, str] = {}
315
- if "person" in self.entities_to_anonymize:
316
- # lookahead/lookbehind به جای \b برای فارسی
317
- patterns["person"] = r'(?<![ء-یa-zA-Z])[ء-ی]+\s+[ء-ی]+(?:\s+[ء-ی]+)*(?![ء-یa-zA-Z])'
318
- if "company" in self.entities_to_anonymize:
319
- patterns["company"] = (
320
- r'(?:(?:شرکت|بانک|سازمان|گروه|هلدینگ|صندوق)\s+)?'
321
- r'[ء-ی][ء-ی\s]+(?:تومان|ریال|دلار)?\s*(?=[\s،؛.!?]|$)'
322
- )
323
- if "amount" in self.entities_to_anonymize:
324
- patterns["amount"] = (
325
- r'[\d۰-۹][,،\d۰-۹]*(?:\.\d+)?'
326
- r'\s*(?:هزار\s+و\s+\d+|هزار|میلیون|میلیارد|همت|��ن|دستگاه)?'
327
- r'\s*(?:میلیارد|میلیون|هزار|تومان|ریال|دلار|یورو|دستگاه|تن|واحد)?'
328
- )
329
- if "percent" in self.entities_to_anonymize:
330
- patterns["percent"] = r'[\d۰-۹]+(?:\.\d+)?\s*(?:درصد|%|درصدی)'
331
-
332
- # استخراج موجودیت‌های اصلی
333
- original_entities: Dict[str, List[str]] = {}
334
- for etype, pat in patterns.items():
335
- matches = re.findall(pat, original)
336
- original_entities[etype] = [m.strip() for m in matches if m.strip()]
337
-
338
- # ربط توکن → مقدار اصلی
339
- for etype in self.entities_to_anonymize:
340
- tokens = sorted(
341
- set(re.findall(rf'{etype}-(\d+)', anonymized)),
342
- key=lambda x: int(x)
343
- )
344
- values = original_entities.get(etype, [])
345
- for tok_num in tokens:
346
- token = f"{etype}-{tok_num}"
347
- idx = int(tok_num) - 1
348
- if idx < len(values):
349
- self.mapping_table[token] = values[idx]
350
- elif values:
351
- self.mapping_table[token] = values[-1]
352
-
353
- self.reverse_mapping = {v: k for k, v in self.mapping_table.items()}
354
- logger.info(f"✅ Fallback mapping: {len(self.mapping_table)} موجودیت")
355
-
356
- # ── تحلیل با LLM ────────────────────────────────────────
357
-
358
- def analyze_with_llm(self, anonymized_text: str, analysis_prompt: str = None) -> str:
359
- logger.info(f"🤖 {self.llm_provider.upper()} تحلیل...")
360
-
361
- if not analysis_prompt or not analysis_prompt.strip():
362
- return "⚠️ هیچ دستور تحلیل داده نشده است"
363
-
364
- prompt = build_analysis_prompt(
365
- anonymized_text, analysis_prompt, self.entities_to_anonymize
366
- )
367
-
368
- try:
369
- response = self.llm_sender.send(
370
- prompt,
371
- lang="fa",
372
- temperature=0.2,
373
- max_tokens=2000
374
- )
375
- logger.info(f"✅ {self.llm_provider.upper()}: {len(response)} کاراکتر")
376
- return response
377
-
378
- except Exception as e:
379
- logger.error(f"❌ {self.llm_provider.upper()} Exception: {e}")
380
- return f"❌ خطا در ارتباط با {self.llm_provider.upper()}: {str(e)}"
381
-
382
- # ── بازگردانی متن ────────────────────────────────────────
383
-
384
- def restore_text(self, anonymized_text: str) -> str:
385
- """
386
- بازگردانی متن
387
- باگ‌های رفع‌شده:
388
- - حذف _clean_for_restore مخرب
389
- - اصلاح _restore_with_regex (text → restored)
390
- """
391
- logger.info("🔄 بازگردانی متن...")
392
-
393
- if not self.mapping_table:
394
- logger.warning("⚠️ جدول نگاشت خالی")
395
- return anonymized_text
396
-
397
- # STEP 1: normalize توکن‌ها
398
- restored = self._normalize_tokens(anonymized_text)
399
-
400
- # STEP 2: جایگزینی طولانی‌ترین توکن‌ها اول (greedy)
401
- count = 0
402
- for placeholder, original in sorted(
403
- self.mapping_table.items(),
404
- key=lambda x: len(x[0]),
405
- reverse=True
406
- ):
407
- if placeholder in restored:
408
- restored = restored.replace(placeholder, original)
409
- count += 1
410
- logger.info(f" ✅ {placeholder} → {original[:40]}")
411
- else:
412
- logger.warning(f" ⚠️ {placeholder} یافت نشد")
413
-
414
- logger.info(f"✅ {count}/{len(self.mapping_table)} توکن بازگردانی شد")
415
-
416
- # STEP 3: fallback regex برای توکن‌های باقی‌مانده
417
- remaining = [p for p in self.mapping_table if p in restored]
418
- if remaining:
419
- logger.info(f"🔍 fallback برای {len(remaining)} توکن باقی‌مانده...")
420
- restored = self._restore_with_regex(restored)
421
-
422
- return restored
423
-
424
- def _normalize_tokens(self, text: str) -> str:
425
- """
426
- نرمال‌سازی توکن‌ها:
427
- - hyphen های یونیکد → hyphen معمولی
428
- - فاصله داخل توکن حذف می‌شود
429
- - کلمات فارسی چسبیده به توکن جدا می‌شوند
430
- """
431
- normalized = text
432
- unicode_hyphens = r'[\u2010\u2011\u2012\u2013\u2014\u2212]'
433
-
434
- for etype in self.entities_to_anonymize:
435
- # hyphen های یونیکد
436
- normalized = re.sub(
437
- rf'{etype}{unicode_hyphens}(\d+)',
438
- rf'{etype}-\1',
439
- normalized
440
- )
441
- # فاصله اضافی داخل توکن
442
- normalized = re.sub(
443
- rf'{etype}\s+-\s+(\d+)',
444
- rf'{etype}-\1',
445
- normalized
446
- )
447
-
448
- # کلمه فارسی چسبیده به توکن
449
- for etype in self.entities_to_anonymize:
450
- normalized = re.sub(
451
- rf'({etype}-\d+)([ء-ی])',
452
- r'\1 \2',
453
- normalized
454
- )
455
- # نشانه‌گذاری چسبیده
456
- normalized = re.sub(
457
- rf'({etype}-\d+)([،؛:.!?])',
458
- r'\1 \2',
459
- normalized
460
- )
461
-
462
- return normalized
463
-
464
- def _restore_with_regex(self, text: str) -> str:
465
- """
466
- fallback برای توکن‌هایی که با replace ساده پیدا نشدند
467
- باگ رفع‌شده:
468
- - بررسی روی `restored` (نه `text` ثابت)
469
- - فقط توکن داخل match جایگزین می‌شود نه کل عبارت
470
- """
471
- restored = text
472
-
473
- for placeholder, original in self.mapping_table.items():
474
- if placeholder not in restored:
475
- continue # قبلاً restore شده یا نیست
476
-
477
- etype = placeholder.split("-")[0]
478
- num = placeholder.split("-")[1]
479
-
480
- # فقط فاصله اضافی داخل توکن را پوشش می‌دهیم
481
- pattern = rf'{etype}\s*-\s*{num}'
482
- if re.search(pattern, restored):
483
- restored = re.sub(pattern, original, restored)
484
- logger.info(f" ✅ regex restore: {placeholder} → {original[:40]}")
485
-
486
- return restored
487
-
488
- # ── جدول نگاشت Markdown ──────────────────────────────────
489
-
490
- def get_mapping_table_md(self) -> str:
491
- if not self.mapping_table:
492
- return "### 📋 جدول نگاشت\n\nهیچ موجودیتی شناسایی نشد"
493
-
494
- table = "### 📋 جدول نگاشت\n\n"
495
- table += "| شناسه | متن اصلی |\n"
496
- table += "|-------|----------|\n"
497
- for token, original in sorted(self.mapping_table.items()):
498
- table += f"| **{token}** | {original} |\n"
499
- return table
500
-
501
-
502
- # ─────────────────────────────────────────────────────────────
503
- # متغیر سراسری
504
- # ─────────────────────────────────────────────────────────────
505
- anonymizer: Optional[AnonymizerAdvanced] = None
506
-
507
-
508
- # ─────────────────────────────────────────────────────────────
509
- # تابع اصلی پردازش
510
- # ─────────────────────────────────────────────────────────────
511
- def process(
512
- input_text: str,
513
- analysis_prompt: str,
514
- llm_provider: str,
515
- llm_model: str,
516
- anonymize_all: bool,
517
- anonymize_person: bool,
518
- anonymize_company: bool,
519
- anonymize_amount: bool,
520
- anonymize_percent: bool
521
- ):
522
- global anonymizer
523
-
524
- if not input_text.strip():
525
- return "", "", "", ""
526
-
527
- # ساخت لیست موجودیت‌ها
528
- if anonymize_all:
529
- entities = ["person", "company", "amount", "percent"]
530
- else:
531
- entities = []
532
- if anonymize_person: entities.append("person")
533
- if anonymize_company: entities.append("company")
534
- if anonymize_amount: entities.append("amount")
535
- if anonymize_percent: entities.append("percent")
536
-
537
- if not entities:
538
- return "", "❌ لطفاً حداقل یک موجودیت انتخاب کنید", "", ""
539
-
540
- deepinfra_key = os.getenv("DEEPINFRA_API_KEY")
541
-
542
- # ایجاد یا آپدیت anonymizer
543
- if not anonymizer:
544
- anonymizer = AnonymizerAdvanced(
545
- deepinfra_key,
546
- llm_provider=llm_provider,
547
- llm_model=llm_model,
548
- entities_to_anonymize=entities
549
- )
550
- else:
551
- anonymizer.set_llm_provider(llm_provider, llm_model, entities)
552
- anonymizer.mapping_table = {}
553
- anonymizer.reverse_mapping = {}
554
-
555
- try:
556
- logger.info("=" * 60)
557
- logger.info(f"🚀 پردازش — {llm_provider} ({llm_model})")
558
- logger.info(f"🎯 موجودیت‌ها: {entities}")
559
- logger.info("=" * 60)
560
-
561
- # مرحله ۱: ناشناس‌سازی
562
- logger.info("🔐 مرحله ۱: ناشناس‌سازی...")
563
- anon_text, _ = anonymizer.anonymize_with_deepinfra(input_text)
564
-
565
- # مرحله ۲: تحلیل LLM
566
- has_analysis = bool(analysis_prompt and analysis_prompt.strip())
567
-
568
- if has_analysis:
569
- logger.info(f"🤖 مرحله ۲: تحلیل {llm_provider.upper()}...")
570
- llm_response = anonymizer.analyze_with_llm(anon_text, analysis_prompt)
571
- else:
572
- llm_response = "⚠️ هیچ دستور تحلیل داده نشده است"
573
-
574
- # مرحله ۳: بازگردانی
575
- logger.info("🔄 مرحله ۳: بازگردانی...")
576
- source_for_restore = llm_response if has_analysis else anon_text
577
- restored = anonymizer.restore_text(source_for_restore)
578
-
579
- # مرحله ۴: جدول نگاشت
580
- mapping_str = anonymizer.get_mapping_table_md()
581
-
582
- logger.info("✅ پردازش کامل")
583
- return restored, llm_response, anon_text, mapping_str
584
-
585
- except Exception as e:
586
- logger.error(f"❌ خطا: {e}", exc_info=True)
587
- return "", f"❌ خطا: {str(e)}", "", ""
588
-
589
-
590
- def clear_all():
591
- return "", "", "", "", "", "", True, False, False, False, False
592
-
593
-
594
- # ─────────────────────────────────────────────────────────────
595
- # رابط کاربری Gradio
596
- # ─────────────────────────────────────────────────────────────
597
- css_rtl = """
598
- .input-box { direction: rtl; text-align: right; }
599
- .textbox textarea {
600
- direction: rtl;
601
- text-align: right;
602
- font-family: 'Tahoma', serif;
603
- }
604
- .thick-divider { border-top: 2px solid #333; margin: 10px 0; }
605
- .compact-checkbox label {
606
- padding: 5px 10px !important;
607
- margin: 3px 0 !important;
608
- font-size: 0.95em !important;
609
- }
610
- """
611
-
612
- with gr.Blocks(title="سیستم ناشناس‌سازی متون", theme=gr.themes.Soft(), css=css_rtl) as app:
613
-
614
- gr.Markdown("# 🔐 پلتفرم امن چت با مدل‌های متنوع و ناشناس‌سازی داده‌ها",
615
- elem_classes="input-box")
616
-
617
- # ── ردیف اول: تنظیمات ──────────────────────────────────
618
- with gr.Row():
619
- with gr.Column(scale=1):
620
- with gr.Group():
621
- gr.Markdown("### ⚙️ تنظیمات مدل", elem_classes="input-box")
622
- llm_provider = gr.Dropdown(
623
- choices=["chatgpt", "grok", "deepinfra"],
624
- value="chatgpt",
625
- label="🤖 انتخاب مدل زبانی",
626
- interactive=True
627
- )
628
- llm_model = gr.Dropdown(
629
- choices=AVAILABLE_MODELS["chatgpt"],
630
- value="gpt-4o-mini",
631
- label="📦 انتخاب نسخه مدل",
632
- interactive=True
633
- )
634
-
635
- with gr.Column(scale=1):
636
- with gr.Group():
637
- gr.Markdown("### 🎯 انتخاب موجودیت‌ها", elem_classes="input-box")
638
- anonymize_all = gr.Checkbox(
639
- label="✅ همه موجودیت‌ها", value=True,
640
- elem_classes="input-box compact-checkbox"
641
- )
642
- anonymize_person = gr.Checkbox(
643
- label="👤 اسامی اشخاص", value=False,
644
- elem_classes="input-box compact-checkbox"
645
- )
646
- anonymize_company = gr.Checkbox(
647
- label="🏢 نام شرکت‌ها", value=False,
648
- elem_classes="input-box compact-checkbox"
649
- )
650
- anonymize_amount = gr.Checkbox(
651
- label="💰 ارقام مالی", value=False,
652
- elem_classes="input-box compact-checkbox"
653
- )
654
- anonymize_percent = gr.Checkbox(
655
- label="📊 درصدها", value=False,
656
- elem_classes="input-box compact-checkbox"
657
- )
658
-
659
- gr.Markdown("---", elem_classes="thick-divider")
660
-
661
- # ── ردیف دوم: ورودی ────────────────────────────────────
662
- with gr.Row():
663
- with gr.Column(scale=1):
664
- gr.Markdown("### 📋 دستورات پردازش", elem_classes="input-box")
665
- analysis_prompt = gr.Textbox(
666
- lines=22,
667
- placeholder="مثال: این متن را خلاصه کن\nیا: نکات کلیدی را استخراج کن",
668
- label="📋 دستورات LLM (اختیاری)",
669
- elem_classes="textbox"
670
- )
671
-
672
- with gr.Column(scale=1):
673
- gr.Markdown("### 📝 متن ورودی", elem_classes="input-box")
674
- input_text = gr.Textbox(
675
- lines=22,
676
- placeholder="متن مالی/خبری را وارد کنید...",
677
- label="",
678
- elem_classes="textbox"
679
- )
680
-
681
- # ── دکمه‌ها ─────────────────────────────────────────────
682
- with gr.Row():
683
- process_btn = gr.Button("▶️ پردازش", variant="primary", size="lg", scale=2)
684
- clear_btn = gr.Button("🗑️ پاک کردن", variant="stop", size="lg", scale=1)
685
-
686
- # ── نتایج ────────────────────────────────────────────────
687
- gr.Markdown("## 📊 نتایج پردازش", elem_classes="input-box")
688
-
689
- with gr.Row():
690
- with gr.Column(scale=1):
691
- restored_text = gr.Textbox(
692
- lines=12, label="✅ متن بازگردانی شده",
693
- interactive=False, elem_classes="textbox"
694
- )
695
- with gr.Column(scale=1):
696
- llm_analysis = gr.Textbox(
697
- lines=12, label="🤖 تحلیل LLM",
698
- interactive=False, elem_classes="textbox"
699
- )
700
- with gr.Column(scale=1):
701
- anonymized_output = gr.Textbox(
702
- lines=12, label="🔒 متن ناشناس‌شده",
703
- interactive=False, elem_classes="textbox"
704
- )
705
-
706
- mapping_table = gr.Markdown(
707
- value="### 📋 جدول نگاشت\n\nهنوز پردازشی انجام نشده",
708
- label="📋 جدول نگاشت",
709
- elem_classes="input-box"
710
- )
711
-
712
- # ── Event handlers ────────────────────────────────────────
713
-
714
- def handle_provider_change(provider):
715
- models = AVAILABLE_MODELS.get(provider, [])
716
- return gr.update(choices=models, value=models[0] if models else None)
717
-
718
- llm_provider.change(
719
- fn=handle_provider_change,
720
- inputs=[llm_provider],
721
- outputs=[llm_model]
722
- )
723
-
724
- def handle_select_all(select_all):
725
- state = gr.update(value=False, interactive=not select_all)
726
- return state, state, state, state
727
-
728
- anonymize_all.change(
729
- fn=handle_select_all,
730
- inputs=[anonymize_all],
731
- outputs=[anonymize_person, anonymize_company, anonymize_amount, anonymize_percent]
732
- )
733
-
734
- process_btn.click(
735
- fn=process,
736
- inputs=[
737
- input_text, analysis_prompt, llm_provider, llm_model,
738
- anonymize_all, anonymize_person, anonymize_company,
739
- anonymize_amount, anonymize_percent
740
- ],
741
- outputs=[restored_text, llm_analysis, anonymized_output, mapping_table]
742
- )
743
-
744
- clear_btn.click(
745
- fn=clear_all,
746
- outputs=[
747
- input_text, analysis_prompt, restored_text, llm_analysis,
748
- anonymized_output, mapping_table,
749
- anonymize_all, anonymize_person, anonymize_company,
750
- anonymize_amount, anonymize_percent
751
- ]
752
- )
753
-
754
-
755
- # ─────────────────────────────────────────────────────────────
756
- if __name__ == "__main__":
757
- print("=" * 70)
758
- print("🚀 سیستم ناشناس‌سازی در حال راه‌اندازی...")
759
- print("=" * 70)
760
- app.launch(
761
- server_name="0.0.0.0",
762
- server_port=7860,
763
- share=False,
764
- show_error=True
765
- )