leilaghomashchi commited on
Commit
6ceefb7
·
verified ·
1 Parent(s): 1f34f4d

Upload local_qwen_anonymizer.py

Browse files
Files changed (1) hide show
  1. local_qwen_anonymizer.py +446 -0
local_qwen_anonymizer.py ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import gradio as gr
3
+ from typing import Dict, Any
4
+ 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: در پاسخ نهایی خود، فقط و فقط متن ناشناس‌سازی شده را برگردانید، بدون هیچ توضیح، تحلیل، یا تگ اضافی.
58
+
59
+ ## قوانین اندیس‌گذاری:
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**: شرکت‌ها، بانک‌ها، سازمان‌ها
67
+ - **person-XX**: نام و نام خانوادگی اشخاص
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 {
125
+ "success": True,
126
+ "anonymized_text": content,
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"]
149
+ content = message["content"]
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 = []
169
+ skip = False
170
+ for line in lines:
171
+ if line.strip().startswith('```'):
172
+ skip = not skip
173
+ continue
174
+ if not skip:
175
+ clean_lines.append(line)
176
+ content = '\n'.join(clean_lines)
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)
197
+ percents = re.findall(r'percent-(\d+)', text)
198
+
199
+ statistics = {
200
+ "company": len(set(companies)),
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 = {
208
+ "companies": sorted(list(set(companies)), key=lambda x: int(x)),
209
+ "persons": sorted(list(set(persons)), key=lambda x: int(x)),
210
+ "amounts": sorted(list(set(amounts)), key=lambda x: int(x)),
211
+ "percents": sorted(list(set(percents)), key=lambda x: int(x))
212
+ }
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 {
222
+ "statistics": statistics,
223
+ "entities": entities,
224
+ "detailed_analysis": detailed_analysis
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)
232
+ percents = re.findall(r'percent-(\d+)', text)
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,
253
+ "issues": validation_issues,
254
+ "entity_counts": {
255
+ "company": len(set(companies)),
256
+ "person": len(set(persons)),
257
+ "amount": len(set(amounts)),
258
+ "percent": len(set(percents))
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;
271
+ direction: rtl;
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 {
283
+ background-color: #e8f5e9;
284
+ border: 2px solid #4caf50;
285
+ border-radius: 12px;
286
+ padding: 15px;
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="📝 متن ورودی",
328
+ placeholder="متن خود را اینجا وارد کنید...",
329
+ lines=12,
330
+ max_lines=25
331
+ )
332
+
333
+ with gr.Row():
334
+ anonymize_btn = gr.Button("🔒 ناشناس‌سازی", variant="primary", size="lg")
335
+ clear_btn = gr.Button("🗑️ پاک کردن", variant="secondary")
336
+
337
+ with gr.Column(scale=1):
338
+ output_text = gr.Textbox(
339
+ label="🎯 متن ناشناس‌سازی شده",
340
+ lines=12,
341
+ max_lines=25,
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,
439
+ label="📚 مثال‌ها"
440
+ )
441
+
442
+ return interface
443
+
444
+ if __name__ == "__main__":
445
+ interface = create_local_interface()
446
+ interface.launch(show_error=True)