leilaghomashchi commited on
Commit
bec85f3
·
verified ·
1 Parent(s): b67aaa4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +150 -171
app.py CHANGED
@@ -5,53 +5,65 @@ import os
5
  from dataclasses import dataclass
6
  import re
7
  from llama_cpp import Llama
 
8
 
9
  @dataclass
10
  class LocalModelConfig:
11
  """تنظیمات مدل محلی GGUF - Qwen2.5-32B"""
12
- model_path: str = r"C:\models\qwen2.5-32B-Instruct-Q4_K_M.gguf"
 
13
  max_tokens: int = 8000
14
  temperature: float = 0.3
15
  top_p: float = 0.8
16
- n_ctx: int = 4096 # حجم context window
17
- n_threads: int = 8 # تعداد CPU threads
18
- n_gpu_layers: int = 45 # برای GPU acceleration
19
 
20
  class LocalCerebrasAnonymizer:
21
  """سیستم ناشناس‌سازی متون مالی فارسی با مدل محلی"""
22
 
23
- def __init__(self, model_path: str = None):
24
- if model_path is None:
25
- # استفاده از مسیر default یا متغیر محیطی
26
- model_path = os.getenv("QWEN_MODEL_PATH", self.config.model_path)
27
-
28
- if not os.path.exists(model_path):
29
- raise ValueError(f" فایل مدل یافت نشد: {model_path}\n💡 لطفاً مسیر صحیح را بررسی کنید")
30
-
31
- self.config = LocalModelConfig(model_path=model_path)
32
-
33
  try:
34
- print(f"🤖 درحال بارگذاری مدل: {model_path}...")
35
- print(f"📊 تنظیمات:")
36
- print(f" Context: {self.config.n_ctx}")
37
- print(f" • Threads: {self.config.n_threads}")
38
- print(f" • GPU Layers: {self.config.n_gpu_layers}")
 
 
 
 
 
 
 
 
 
39
 
40
  self.llm = Llama(
41
- model_path=self.config.model_path,
42
  n_ctx=self.config.n_ctx,
43
  n_threads=self.config.n_threads,
44
  n_gpu_layers=self.config.n_gpu_layers,
45
  verbose=False
46
  )
 
 
47
  print("✅ مدل با موفقیت بارگذاری شد\n")
 
 
48
  except Exception as e:
49
- raise Exception(f"❌ خطا در بارگذاری مدل: {str(e)}")
50
-
51
- self.system_prompt = self._create_advanced_system_prompt()
52
 
53
- def _create_advanced_system_prompt(self) -> str:
54
- """دستورالعمل سیستمی بهینه شده"""
55
  return """شما یک سیستم ناشناس‌سازی متون مالی فارسی هستید.
56
 
57
  ⚠️ CRITICAL: در پاسخ نهایی خود، فقط و فقط متن ناشناس‌سازی شده را برگردانید، بدون هیچ توضیح، تحلیل، یا تگ اضافی.
@@ -60,7 +72,6 @@ class LocalCerebrasAnonymizer:
60
  1. **ترتیب پیوسته**: company-01, company-02, ... | person-01, person-02, ... | amount-01, amount-02, ... | percent-01, percent-02, ...
61
  2. **ثبات**: اگر "همراه اول" → company-01 شد، در تمام متن همان باشد
62
  3. **نام مستعار**: "فاما" = "فولاد مبارکه" → هر دو company-01
63
- 4. **اشاره ضمنی**: "این شرکت" اگر به company-01 اشاره دارد → company-01
64
 
65
  ## انواع موجودیت:
66
  - **company-XX**: شرکت‌ها، بانک‌ها، سازمان‌ها
@@ -68,57 +79,46 @@ class LocalCerebrasAnonymizer:
68
  - **amount-XX**: مبالغ - واحد را حفظ کن
69
  - **percent-XX**: درصدها
70
 
71
- ## قوانین کلیدی:
72
- 1. بازرس = شرکت است → company-XX
73
- 2. واحدها: "amount-01 میلیارد تومان" ✅
74
- 3. گروه‌ها: "گروه X" → company-XX
75
- 4. کلمات عمومی حفظ: "سه شرکت" → حفظ
76
- 5. دوره زمانی حفظ: "۵ ماهه" → حفظ
77
-
78
  ## مثال:
79
  ورودی: ایران خودرو در اسفند 1402 حدود 23 هزار میلیارد درآ��د کسب کرد که 4.58 درصد افزایش داشت.
80
  خروجی: company-01 در اسفند 1402 حدود amount-01 درآمد کسب کرد که percent-01 افزایش داشت.
81
 
82
- ⚠️ یادآوری: فقط متن ناشناس‌شده، بدون هیچ توضیح اضافی."""
83
 
84
  def anonymize_text(self, text: str) -> Dict[str, Any]:
85
- """ناشناس‌سازی متن با استفاده از مدل محلی"""
 
 
 
86
  if not text.strip():
87
- return {
88
- "success": False,
89
- "error": "متن ورودی خالی است"
90
- }
91
 
92
  try:
93
- # ایجاد پیام برای مدل
94
  messages = [
95
- {"role": "system", "content": self.system_prompt},
96
  {"role": "user", "content": text}
97
  ]
98
 
99
- # تبدیل پیام‌ها به فرمت مناسب
100
  prompt = self._format_prompt(messages)
101
 
102
  print(f"⏳ پردازش متن... (طول: {len(text)} کاراکتر)")
103
 
104
- # فراخوانی مدل
105
  response = self.llm(
106
  prompt,
107
  max_tokens=self.config.max_tokens,
108
  temperature=self.config.temperature,
109
  top_p=self.config.top_p,
110
- stop=["</s>", "### User:", "[INST]"]
111
  )
112
 
113
  content = response["choices"][0]["text"].strip()
114
 
115
- # پاک‌سازی خروجی
116
  content = self._remove_thinking_tags(content)
117
  content = self._clean_markdown(content)
118
  content = self._clean_explanations(content)
119
  content = content.strip()
120
 
121
- # تحلیل نتایج
122
  analysis = self._analyze_anonymized_text(content)
123
 
124
  return {
@@ -127,22 +127,14 @@ class LocalCerebrasAnonymizer:
127
  "entities": analysis["entities"],
128
  "statistics": analysis["statistics"],
129
  "detailed_analysis": analysis["detailed_analysis"],
130
- "usage": {
131
- "prompt_tokens": response.get("usage", {}).get("prompt_tokens", 0),
132
- "completion_tokens": response.get("usage", {}).get("completion_tokens", 0),
133
- "total_tokens": response.get("usage", {}).get("total_tokens", 0)
134
- },
135
  "quality_check": self._validate_anonymized_text(content)
136
  }
137
 
138
  except Exception as e:
139
- return {
140
- "success": False,
141
- "error": f"خطا در پردازش: {str(e)}"
142
- }
143
 
144
  def _format_prompt(self, messages: list) -> str:
145
- """تبدیل پیام‌ها به فرمت Qwen"""
146
  formatted = ""
147
  for message in messages:
148
  role = message["role"]
@@ -150,19 +142,17 @@ class LocalCerebrasAnonymizer:
150
  if role == "system":
151
  formatted += f"{content}\n\n"
152
  elif role == "user":
153
- formatted += f"### User:\n{content}\n\n### Assistant:\n"
154
  elif role == "assistant":
155
  formatted += f"{content}\n\n"
156
  return formatted
157
 
158
  def _remove_thinking_tags(self, content: str) -> str:
159
- """حذف تگ‌های thinking"""
160
  content = re.sub(r'<think>.*?</think>', '', content, flags=re.DOTALL)
161
  content = re.sub(r'</?think>', '', content)
162
  return content.strip()
163
 
164
  def _clean_markdown(self, content: str) -> str:
165
- """پاک کردن markdown"""
166
  if "```" in content:
167
  lines = content.split('\n')
168
  clean_lines = []
@@ -177,20 +167,16 @@ class LocalCerebrasAnonymizer:
177
  return content
178
 
179
  def _clean_explanations(self, content: str) -> str:
180
- """حذف توضیحات اضافی"""
181
  lines = content.split('\n')
182
  clean_lines = []
183
-
184
  for line in lines:
185
  if any(word in line.lower() for word in
186
- ['okay', 'let me', 'here is', 'خروجی', 'نتیجه', 'پاسخ:', 'assistant']):
187
  continue
188
  clean_lines.append(line)
189
-
190
  return '\n'.join(clean_lines).strip()
191
 
192
  def _analyze_anonymized_text(self, text: str) -> Dict[str, Any]:
193
- """تحلیل متن ناشناس‌سازی شده"""
194
  companies = re.findall(r'company-(\d+)', text)
195
  persons = re.findall(r'person-(\d+)', text)
196
  amounts = re.findall(r'amount-(\d+)', text)
@@ -201,7 +187,7 @@ class LocalCerebrasAnonymizer:
201
  "person": len(set(persons)),
202
  "amount": len(set(amounts)),
203
  "percent": len(set(percents)),
204
- "total_replacements": len(companies) + len(persons) + len(amounts) + len(percents)
205
  }
206
 
207
  entities = {
@@ -213,9 +199,8 @@ class LocalCerebrasAnonymizer:
213
 
214
  detailed_analysis = {
215
  "preserved_dates": len(re.findall(r'\d{4}/\d{1,2}/\d{1,2}|\d{1,2}\s+\w+\s+\d{4}', text)),
216
- "preserved_times": len(re.findall(r'\d{1,2}:\d{2}', text)),
217
  "financial_indicators": len(re.findall(r'\b(EPS|P/E|ARPU|NPL|ROE|ROA)\b', text)),
218
- "units_preserved": len(re.findall(r'(میلیارد|میلیون|هزار|تومان|ریال|درهم|دلار|یورو)', text))
219
  }
220
 
221
  return {
@@ -225,7 +210,6 @@ class LocalCerebrasAnonymizer:
225
  }
226
 
227
  def _validate_anonymized_text(self, text: str) -> Dict[str, Any]:
228
- """اعتبارسنجی متن"""
229
  companies = re.findall(r'company-(\d+)', text)
230
  persons = re.findall(r'person-(\d+)', text)
231
  amounts = re.findall(r'amount-(\d+)', text)
@@ -233,20 +217,16 @@ class LocalCerebrasAnonymizer:
233
 
234
  validation_issues = []
235
 
236
- for entity_type, indices in [
237
- ("company", companies),
238
- ("person", persons),
239
- ("amount", amounts),
240
- ("percent", percents)
241
- ]:
242
  if indices:
243
  unique_indices = sorted(list(set([int(x) for x in indices])))
244
  if unique_indices[0] != 1:
245
- validation_issues.append(f"اندیس {entity_type} از 01 شروع نشده")
246
 
247
  expected = list(range(1, len(unique_indices) + 1))
248
  if unique_indices != expected:
249
- validation_issues.append(f"اندیس‌های {entity_type} پیوسته نیستند")
250
 
251
  return {
252
  "is_valid": len(validation_issues) == 0,
@@ -259,12 +239,11 @@ class LocalCerebrasAnonymizer:
259
  }
260
  }
261
 
262
- def create_local_interface():
263
- """رابط کاربری برای مدل محلی"""
264
-
265
- default_path = r"C:\models\qwen2.5-32B-Instruct-Q4_K_M.gguf"
266
- model_available = os.path.exists(default_path)
267
-
268
  custom_css = """
269
  .gradio-container {
270
  font-family: 'Tahoma', 'Arial', sans-serif !important;
@@ -272,11 +251,12 @@ def create_local_interface():
272
  max-width: 1400px;
273
  margin: 0 auto;
274
  }
275
- .result-box {
276
- background-color: #f8f9fa;
277
- border: 2px solid #e9ecef;
278
  border-radius: 12px;
279
- padding: 20px;
 
280
  margin: 10px 0;
281
  }
282
  .local-box {
@@ -287,41 +267,35 @@ def create_local_interface():
287
  color: #1b5e20;
288
  margin: 10px 0;
289
  }
290
- .info-box {
291
- background-color: #e3f2fd;
292
- border: 2px solid #2196F3;
293
  border-radius: 12px;
294
- padding: 15px;
295
- color: #0d47a1;
296
- margin: 10px 0;
297
- font-size: 12px;
298
  }
299
  """
300
 
301
- with gr.Blocks(css=custom_css, title="ناشناس‌ساز Qwen 2.5-32B", theme=gr.themes.Soft()) as interface:
302
 
303
  gr.Markdown("""
304
  # 🔒 سیستم ناشناس‌سازی متون مالی فارسی
305
- ### 🖥️ مدل: Qwen 2.5-32B (محلی Q4_K_M)
306
  """)
307
 
308
- gr.Markdown(f"""
309
  <div class="info-box">
310
- 📂 <strong>مسیر مدل:</strong> {default_path}<br>
311
- <strong>وضعیت:</strong> {"✅ مدل دست‌رس است" if model_available else "❌ مدل یافت نشد"}
 
 
312
  </div>
313
  """)
314
 
315
- gr.Markdown("""
316
- <div class="local-box">
317
- 🚀 <strong>مدل: Qwen2.5-32B-Instruct (Q4_K_M)</strong><br>
318
- ✅ <strong>مزیت:</strong> بدون اینترنت • بدون هزینه API • کنترل کامل<br>
319
- ⚡ <strong>نیاز سیستم:</strong> 16+ GB RAM یا GPU VRAM<br>
320
- 📊 <strong>Quantization:</strong> Q4 (معادل 20 GB مدل کامل)
321
- </div>
322
- """)
323
 
324
- with gr.Row():
325
  with gr.Column(scale=1):
326
  input_text = gr.Textbox(
327
  label="📝 متن ورودی",
@@ -342,97 +316,102 @@ def create_local_interface():
342
  elem_classes=["result-box"]
343
  )
344
 
345
- with gr.Row():
346
- statistics_output = gr.Markdown(label="📊 آمار")
347
- quality_output = gr.Markdown(label=" کیفیت")
 
 
348
 
349
- with gr.Row():
350
  entities_output = gr.Markdown(label="🏷️ موجودیت‌ها")
351
- detailed_analysis_output = gr.Markdown(label="🔍 تحلیل")
352
 
353
- status_output = gr.Textbox(label="📋 وضعیت", interactive=False)
 
 
 
 
 
 
 
 
354
 
355
- def process_local_text(text: str):
356
  """پردازش متن"""
357
- final_model_path = default_path
 
358
 
359
- if not os.path.exists(final_model_path):
360
- return ("", f"❌ فایل مدل یافت نشد", "", "", "", f"❌ مسیر: {final_model_path}")
361
 
362
- if not text or not text.strip():
363
- return ("", "❌ متن ورودی خالی است", "", "", "", "❌ خطا")
364
 
365
- try:
366
- anonymizer = LocalCerebrasAnonymizer(model_path=final_model_path)
367
- result = anonymizer.anonymize_text(text)
368
-
369
- if not result["success"]:
370
- return ("", f"❌ خطا: {result['error']}", "", "", "", "❌ خطا")
371
-
372
- stats = result.get("statistics", {})
373
- stats_md = f"""📊 **آمار:**
374
  🏢 شرکت: {stats.get('company', 0)}
375
  👤 اشخاص: {stats.get('person', 0)}
376
  💰 مبالغ: {stats.get('amount', 0)}
377
  📊 درصدها: {stats.get('percent', 0)}
378
- 🔢 کل: {stats.get('total_replacements', 0)}
 
 
 
 
 
379
  """
380
-
381
- quality = result.get("quality_check", {})
382
- quality_md = "✅ **کنترل کیفیت:**\n\n"
383
- quality_md += " موفق" if quality.get("is_valid", False) else "❌ مشکل"
384
- if quality.get("issues"):
385
- quality_md += "\n\n**مشکلات:**\n"
386
- for issue in quality["issues"]:
387
- quality_md += f"• {issue}\n"
388
-
389
- entities = result.get("entities", {})
390
- entities_md = "🏷️ **موجودیت‌ها:**\n"
391
- if entities.get("companies"):
392
- entities_md += f"\n🏢 company-{', company-'.join(entities['companies'])}"
393
- if entities.get("persons"):
394
- entities_md += f"\n👤 person-{', person-'.join(entities['persons'])}"
395
- if entities.get("amounts"):
396
- entities_md += f"\n💰 amount-{', amount-'.join(entities['amounts'])}"
397
- if entities.get("percents"):
398
- entities_md += f"\n📊 percent-{', percent-'.join(entities['percents'])}"
399
-
400
- detailed = result.get("detailed_analysis", {})
401
- detailed_md = f"""🔍 **تحلیل:**
402
  📅 تاریخ: {detailed.get('preserved_dates', 0)}
403
  📈 شاخص: {detailed.get('financial_indicators', 0)}
404
- 📏 واحد: {detailed.get('units_preserved', 0)}
405
- """
406
-
407
- return (
408
- result["anonymized_text"],
409
- stats_md,
410
- quality_md,
411
- entities_md,
412
- detailed_md,
413
- "✅ موفق"
414
- )
415
-
416
- except Exception as e:
417
- return ("", f"❌ خطا: {str(e)}", "", "", "", f"❌ {str(e)}")
418
 
419
  def clear_all():
420
  return "", "", "", "", "", ""
421
 
 
 
 
 
 
422
  anonymize_btn.click(
423
- fn=process_local_text,
424
  inputs=[input_text],
425
- outputs=[output_text, statistics_output, quality_output, entities_output, detailed_analysis_output, status_output]
426
  )
427
 
428
  clear_btn.click(
429
  fn=clear_all,
430
- outputs=[input_text, output_text, statistics_output, quality_output, entities_output, detailed_analysis_output]
431
  )
432
 
433
  gr.Examples(
434
  examples=[
435
- ["ایران خودرو در اسفندماه سال 1402 حدود 23 هزار و 296 میلیارد تومان درآمد کسب کرد که 4.58 درصد افزایش داشت."],
436
  ["مجمع پتروشیمی برگزار شد. وانیا نیک تدبیر را بازرس انتخاب کردند."],
437
  ],
438
  inputs=input_text,
@@ -442,5 +421,5 @@ def create_local_interface():
442
  return interface
443
 
444
  if __name__ == "__main__":
445
- interface = create_local_interface()
446
- interface.launch(show_error=True)
 
5
  from dataclasses import dataclass
6
  import re
7
  from llama_cpp import Llama
8
+ from huggingface_hub import hf_hub_download
9
 
10
  @dataclass
11
  class LocalModelConfig:
12
  """تنظیمات مدل محلی GGUF - Qwen2.5-32B"""
13
+ repo_id: str = "Qwen/Qwen2.5-32B-Instruct-GGUF"
14
+ filename: str = "qwen2.5-32b-instruct-q4_k_m.gguf"
15
  max_tokens: int = 8000
16
  temperature: float = 0.3
17
  top_p: float = 0.8
18
+ n_ctx: int = 4096
19
+ n_threads: int = 4 # کمتر برای Spaces
20
+ n_gpu_layers: int = 50
21
 
22
  class LocalCerebrasAnonymizer:
23
  """سیستم ناشناس‌سازی متون مالی فارسی با مدل محلی"""
24
 
25
+ def __init__(self):
26
+ self.config = LocalModelConfig()
27
+ self.llm = None
28
+ self.model_loaded = False
29
+
30
+ def load_model(self) -> str:
31
+ """بارگذاری مدل از HuggingFace"""
 
 
 
32
  try:
33
+ print(f"🤖 درحال دانلود مدل از HuggingFace...")
34
+ print(f"📦 Repo: {self.config.repo_id}")
35
+ print(f"📄 Filename: {self.config.filename}")
36
+
37
+ # دانلود مدل
38
+ model_path = hf_hub_download(
39
+ repo_id=self.config.repo_id,
40
+ filename=self.config.filename,
41
+ local_dir="./models",
42
+ local_dir_use_symlinks=False
43
+ )
44
+
45
+ print(f"✅ مدل دانلود شد: {model_path}")
46
+ print(f"🤖 درحال بارگذاری مدل...")
47
 
48
  self.llm = Llama(
49
+ model_path=model_path,
50
  n_ctx=self.config.n_ctx,
51
  n_threads=self.config.n_threads,
52
  n_gpu_layers=self.config.n_gpu_layers,
53
  verbose=False
54
  )
55
+
56
+ self.model_loaded = True
57
  print("✅ مدل با موفقیت بارگذاری شد\n")
58
+ return "✅ مدل آماده است"
59
+
60
  except Exception as e:
61
+ error_msg = f"❌ خطا: {str(e)}"
62
+ print(error_msg)
63
+ return error_msg
64
 
65
+ def _create_system_prompt(self) -> str:
66
+ """دستورالعمل سیستمی"""
67
  return """شما یک سیستم ناشناس‌سازی متون مالی فارسی هستید.
68
 
69
  ⚠️ CRITICAL: در پاسخ نهایی خود، فقط و فقط متن ناشناس‌سازی شده را برگردانید، بدون هیچ توضیح، تحلیل، یا تگ اضافی.
 
72
  1. **ترتیب پیوسته**: company-01, company-02, ... | person-01, person-02, ... | amount-01, amount-02, ... | percent-01, percent-02, ...
73
  2. **ثبات**: اگر "همراه اول" → company-01 شد، در تمام متن همان باشد
74
  3. **نام مستعار**: "فاما" = "فولاد مبارکه" → هر دو company-01
 
75
 
76
  ## انواع موجودیت:
77
  - **company-XX**: شرکت‌ها، بانک‌ها، سازمان‌ها
 
79
  - **amount-XX**: مبالغ - واحد را حفظ کن
80
  - **percent-XX**: درصدها
81
 
 
 
 
 
 
 
 
82
  ## مثال:
83
  ورودی: ایران خودرو در اسفند 1402 حدود 23 هزار میلیارد درآ��د کسب کرد که 4.58 درصد افزایش داشت.
84
  خروجی: company-01 در اسفند 1402 حدود amount-01 درآمد کسب کرد که percent-01 افزایش داشت.
85
 
86
+ ⚠️ یادآوری: فقط متن ناشناس‌شده."""
87
 
88
  def anonymize_text(self, text: str) -> Dict[str, Any]:
89
+ """ناشناس‌سازی متن"""
90
+ if not self.model_loaded:
91
+ return {"success": False, "error": "مدل بارگذاری نشده است"}
92
+
93
  if not text.strip():
94
+ return {"success": False, "error": "متن ورودی خالی است"}
 
 
 
95
 
96
  try:
 
97
  messages = [
98
+ {"role": "system", "content": self._create_system_prompt()},
99
  {"role": "user", "content": text}
100
  ]
101
 
 
102
  prompt = self._format_prompt(messages)
103
 
104
  print(f"⏳ پردازش متن... (طول: {len(text)} کاراکتر)")
105
 
 
106
  response = self.llm(
107
  prompt,
108
  max_tokens=self.config.max_tokens,
109
  temperature=self.config.temperature,
110
  top_p=self.config.top_p,
111
+ stop=["</s>", "[/INST]", "### User:"]
112
  )
113
 
114
  content = response["choices"][0]["text"].strip()
115
 
116
+ # پاک‌سازی
117
  content = self._remove_thinking_tags(content)
118
  content = self._clean_markdown(content)
119
  content = self._clean_explanations(content)
120
  content = content.strip()
121
 
 
122
  analysis = self._analyze_anonymized_text(content)
123
 
124
  return {
 
127
  "entities": analysis["entities"],
128
  "statistics": analysis["statistics"],
129
  "detailed_analysis": analysis["detailed_analysis"],
 
 
 
 
 
130
  "quality_check": self._validate_anonymized_text(content)
131
  }
132
 
133
  except Exception as e:
134
+ return {"success": False, "error": f"خطا: {str(e)}"}
 
 
 
135
 
136
  def _format_prompt(self, messages: list) -> str:
137
+ """فرمت prompt برای Qwen2.5"""
138
  formatted = ""
139
  for message in messages:
140
  role = message["role"]
 
142
  if role == "system":
143
  formatted += f"{content}\n\n"
144
  elif role == "user":
145
+ formatted += f"[INST] {content} [/INST]\n"
146
  elif role == "assistant":
147
  formatted += f"{content}\n\n"
148
  return formatted
149
 
150
  def _remove_thinking_tags(self, content: str) -> str:
 
151
  content = re.sub(r'<think>.*?</think>', '', content, flags=re.DOTALL)
152
  content = re.sub(r'</?think>', '', content)
153
  return content.strip()
154
 
155
  def _clean_markdown(self, content: str) -> str:
 
156
  if "```" in content:
157
  lines = content.split('\n')
158
  clean_lines = []
 
167
  return content
168
 
169
  def _clean_explanations(self, content: str) -> str:
 
170
  lines = content.split('\n')
171
  clean_lines = []
 
172
  for line in lines:
173
  if any(word in line.lower() for word in
174
+ ['okay', 'let me', 'here is', 'خروجی', 'نتیجه', 'پاسخ:', 'assistant', '[inst]']):
175
  continue
176
  clean_lines.append(line)
 
177
  return '\n'.join(clean_lines).strip()
178
 
179
  def _analyze_anonymized_text(self, text: str) -> Dict[str, Any]:
 
180
  companies = re.findall(r'company-(\d+)', text)
181
  persons = re.findall(r'person-(\d+)', text)
182
  amounts = re.findall(r'amount-(\d+)', text)
 
187
  "person": len(set(persons)),
188
  "amount": len(set(amounts)),
189
  "percent": len(set(percents)),
190
+ "total": len(companies) + len(persons) + len(amounts) + len(percents)
191
  }
192
 
193
  entities = {
 
199
 
200
  detailed_analysis = {
201
  "preserved_dates": len(re.findall(r'\d{4}/\d{1,2}/\d{1,2}|\d{1,2}\s+\w+\s+\d{4}', text)),
 
202
  "financial_indicators": len(re.findall(r'\b(EPS|P/E|ARPU|NPL|ROE|ROA)\b', text)),
203
+ "units_preserved": len(re.findall(r'(میلیارد|میلیون|هزار|تومان|ریال|درهم|دلار)', text))
204
  }
205
 
206
  return {
 
210
  }
211
 
212
  def _validate_anonymized_text(self, text: str) -> Dict[str, Any]:
 
213
  companies = re.findall(r'company-(\d+)', text)
214
  persons = re.findall(r'person-(\d+)', text)
215
  amounts = re.findall(r'amount-(\d+)', text)
 
217
 
218
  validation_issues = []
219
 
220
+ for entity_type, indices in [("company", companies), ("person", persons),
221
+ ("amount", amounts), ("percent", percents)]:
 
 
 
 
222
  if indices:
223
  unique_indices = sorted(list(set([int(x) for x in indices])))
224
  if unique_indices[0] != 1:
225
+ validation_issues.append(f"⚠️ {entity_type} از 01 شروع نشده")
226
 
227
  expected = list(range(1, len(unique_indices) + 1))
228
  if unique_indices != expected:
229
+ validation_issues.append(f"⚠️ {entity_type} پیوسته نیست")
230
 
231
  return {
232
  "is_valid": len(validation_issues) == 0,
 
239
  }
240
  }
241
 
242
+ # ========== رابط کاربری ==========
243
+
244
+ anonymizer = LocalCerebrasAnonymizer()
245
+
246
+ def create_interface():
 
247
  custom_css = """
248
  .gradio-container {
249
  font-family: 'Tahoma', 'Arial', sans-serif !important;
 
251
  max-width: 1400px;
252
  margin: 0 auto;
253
  }
254
+ .info-box {
255
+ background-color: #e3f2fd;
256
+ border: 2px solid #2196F3;
257
  border-radius: 12px;
258
+ padding: 15px;
259
+ color: #0d47a1;
260
  margin: 10px 0;
261
  }
262
  .local-box {
 
267
  color: #1b5e20;
268
  margin: 10px 0;
269
  }
270
+ .result-box {
271
+ background-color: #f8f9fa;
272
+ border: 2px solid #e9ecef;
273
  border-radius: 12px;
274
+ padding: 20px;
 
 
 
275
  }
276
  """
277
 
278
+ with gr.Blocks(css=custom_css, title="ناشناس‌ساز Qwen2.5", theme=gr.themes.Soft()) as interface:
279
 
280
  gr.Markdown("""
281
  # 🔒 سیستم ناشناس‌سازی متون مالی فارسی
282
+ ### 🚀 Qwen 2.5-32B (HuggingFace Spaces)
283
  """)
284
 
285
+ gr.Markdown("""
286
  <div class="info-box">
287
+ 📊 <strong>مدل:</strong> Qwen2.5-32B-Instruct-Q4_K_M<br>
288
+ 🌐 <strong>منبع:</strong> HuggingFace Hub<br>
289
+ 💾 <strong>حجم:</strong> ~20 GB (Q4 quantization)<br>
290
+ ⚡ <strong>سرعت:</strong> بستگی به GPU Spaces دارد
291
  </div>
292
  """)
293
 
294
+ status_box = gr.Textbox(label="📋 وضعیت", interactive=False, value="⏳ درحال بارگذاری مدل...")
295
+
296
+ load_btn = gr.Button("🤖 بارگذاری مدل", variant="primary", size="lg")
 
 
 
 
 
297
 
298
+ with gr.Row(visible=False) as input_section:
299
  with gr.Column(scale=1):
300
  input_text = gr.Textbox(
301
  label="📝 متن ورودی",
 
316
  elem_classes=["result-box"]
317
  )
318
 
319
+ with gr.Row(visible=False) as output_section:
320
+ with gr.Column():
321
+ statistics_output = gr.Markdown(label="📊 آمار")
322
+ with gr.Column():
323
+ quality_output = gr.Markdown(label="✅ کیفیت")
324
 
325
+ with gr.Row(visible=False) as output_section2:
326
  entities_output = gr.Markdown(label="🏷️ موجودیت‌ها")
327
+ detailed_output = gr.Markdown(label="🔍 تحلیل")
328
 
329
+ def load_model_action():
330
+ """بارگذاری مدل"""
331
+ msg = anonymizer.load_model()
332
+ return (
333
+ gr.Textbox(value=msg),
334
+ gr.Row(visible=True),
335
+ gr.Row(visible=True),
336
+ gr.Row(visible=True)
337
+ )
338
 
339
+ def process_text(text):
340
  """پردازش متن"""
341
+ if not text.strip():
342
+ return ("", "❌ متن خالی است", "", "", "", "")
343
 
344
+ result = anonymizer.anonymize_text(text)
 
345
 
346
+ if not result["success"]:
347
+ return ("", f"❌ {result['error']}", "", "", "", "")
348
 
349
+ stats = result.get("statistics", {})
350
+ stats_md = f"""📊 **آمار:**
 
 
 
 
 
 
 
351
  🏢 شرکت: {stats.get('company', 0)}
352
  👤 اشخاص: {stats.get('person', 0)}
353
  💰 مبالغ: {stats.get('amount', 0)}
354
  📊 درصدها: {stats.get('percent', 0)}
355
+ 🔢 کل: {stats.get('total', 0)}"""
356
+
357
+ quality = result.get("quality_check", {})
358
+ quality_md = f"""✅ **کنترل کیفیت:**
359
+
360
+ {'✅ موفق' if quality.get('is_valid') else '❌ مشکل'}
361
  """
362
+ if quality.get("issues"):
363
+ quality_md += "\n**مشکلات:**\n"
364
+ for issue in quality["issues"]:
365
+ quality_md += f" {issue}\n"
366
+
367
+ entities = result.get("entities", {})
368
+ entities_md = "🏷️ **موجودیت‌ها:**\n"
369
+ if entities.get("companies"):
370
+ entities_md += f"\n🏢 company-{', company-'.join(entities['companies'])}"
371
+ if entities.get("persons"):
372
+ entities_md += f"\n👤 person-{', person-'.join(entities['persons'])}"
373
+ if entities.get("amounts"):
374
+ entities_md += f"\n💰 amount-{', amount-'.join(entities['amounts'])}"
375
+ if entities.get("percents"):
376
+ entities_md += f"\n📊 percent-{', percent-'.join(entities['percents'])}"
377
+
378
+ detailed = result.get("detailed_analysis", {})
379
+ detailed_md = f"""🔍 **تحلیل:**
 
 
 
 
380
  📅 تاریخ: {detailed.get('preserved_dates', 0)}
381
  📈 شاخص: {detailed.get('financial_indicators', 0)}
382
+ 📏 واحد: {detailed.get('units_preserved', 0)}"""
383
+
384
+ return (
385
+ result["anonymized_text"],
386
+ stats_md,
387
+ quality_md,
388
+ entities_md,
389
+ detailed_md,
390
+ "✅ موفق"
391
+ )
 
 
 
 
392
 
393
  def clear_all():
394
  return "", "", "", "", "", ""
395
 
396
+ load_btn.click(
397
+ fn=load_model_action,
398
+ outputs=[status_box, input_section, output_section, output_section2]
399
+ )
400
+
401
  anonymize_btn.click(
402
+ fn=process_text,
403
  inputs=[input_text],
404
+ outputs=[output_text, statistics_output, quality_output, entities_output, detailed_output, status_box]
405
  )
406
 
407
  clear_btn.click(
408
  fn=clear_all,
409
+ outputs=[input_text, output_text, statistics_output, quality_output, entities_output, detailed_output]
410
  )
411
 
412
  gr.Examples(
413
  examples=[
414
+ ["ایران خودرو در اسفندماه حدود 23 هزار میلیارد تومان درآمد کسب کرد که 4.58 درصد افزایش داشت."],
415
  ["مجمع پتروشیمی برگزار شد. وانیا نیک تدبیر را بازرس انتخاب کردند."],
416
  ],
417
  inputs=input_text,
 
421
  return interface
422
 
423
  if __name__ == "__main__":
424
+ interface = create_interface()
425
+ interface.launch()