KashefTech commited on
Commit
711e5e2
·
verified ·
1 Parent(s): 33cded9

Delete app.py

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