KashefTech commited on
Commit
9ac90ce
·
verified ·
1 Parent(s): f404a42

Upload app (61).py

Browse files
Files changed (1) hide show
  1. app (61).py +765 -0
app (61).py ADDED
@@ -0,0 +1,765 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ )