leilaghomashchi commited on
Commit
2dc4cee
·
verified ·
1 Parent(s): 51ab66c

Upload evaluation_tool1.py

Browse files
Files changed (1) hide show
  1. evaluation_tool1.py +602 -0
evaluation_tool1.py ADDED
@@ -0,0 +1,602 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import re
4
+ from typing import Dict, List, Tuple, Set
5
+ import gradio as gr
6
+ from datetime import datetime
7
+ import io
8
+ import tempfile
9
+ import os
10
+
11
+ class AnonymizationEvaluator:
12
+ """ابزار ارزیابی ناشناس‌سازی با استفاده از متن مرجع"""
13
+
14
+ def __init__(self):
15
+ self.results_df = None
16
+
17
+ def extract_entity_types_from_text(self, text: str) -> Dict[str, int]:
18
+ """استخراج انواع موجودیت‌ها با پشتیبانی از فرمت‌های مختلف"""
19
+ if pd.isna(text) or not isinstance(text, str):
20
+ return {'companies': 0, 'persons': 0, 'amounts': 0, 'percents': 0, 'groups': 0, 'stocks': 0}
21
+
22
+ # تمیز کردن متن
23
+ text = str(text).strip()
24
+
25
+ # الگوهای برای فرمت‌های مختلف
26
+ patterns = {
27
+ 'companies': [
28
+ r'company-\d+', r'Company-\d+', r'COMPANY-\d+', # فرمت استاندارد
29
+ r'COMPANY_\d+(?:_[A-Z]+)?', r'company_\d+(?:_[a-z]+)?' # فرمت regex
30
+ ],
31
+ 'persons': [
32
+ r'person-\d+', r'Person-\d+', r'PERSON-\d+',
33
+ r'PERSON_\d+(?:_[A-Z]+)?', r'person_\d+(?:_[a-z]+)?'
34
+ ],
35
+ 'amounts': [
36
+ r'amount-\d+', r'Amount-\d+', r'AMOUNT-\d+',
37
+ r'AMOUNT_\d+(?:_[A-Z]+)?', r'amount_\d+(?:_[a-z]+)?'
38
+ ],
39
+ 'percents': [
40
+ r'percent-\d+', r'Percent-\d+', r'PERCENT-\d+',
41
+ r'PERCENT_\d+(?:_[A-Z]+)?', r'percent_\d+(?:_[a-z]+)?'
42
+ ],
43
+ 'groups': [
44
+ r'group-\d+', r'Group-\d+', r'GROUP-\d+',
45
+ r'GROUP_\d+(?:_[A-Z]+)?', r'group_\d+(?:_[a-z]+)?'
46
+ ],
47
+ 'stocks': [
48
+ r'stock-\d+', r'Stock-\d+', r'STOCK-\d+',
49
+ r'STOCK_SYMBOL_\d+(?:_[A-Z]+)?', r'stock_symbol_\d+(?:_[a-z]+)?'
50
+ ]
51
+ }
52
+
53
+ entity_counts = {}
54
+ for entity_type, pattern_list in patterns.items():
55
+ count = 0
56
+ for pattern in pattern_list:
57
+ matches = re.findall(pattern, text)
58
+ count += len(matches)
59
+ entity_counts[entity_type] = count
60
+
61
+ return entity_counts
62
+
63
+ def normalize_entity_format(self, text: str) -> str:
64
+ """نرمال‌سازی فرمت‌های مختلف به یک فرمت استاندارد"""
65
+ if pd.isna(text) or not isinstance(text, str):
66
+ return ""
67
+
68
+ # نگاشت انواع مختلف به نام استاندارد
69
+ replacements = [
70
+ # فرمت regex به استاندارد
71
+ (r'COMPANY_(\d+)(?:_[A-Z]+)?', r'company-\1'),
72
+ (r'PERSON_(\d+)(?:_[A-Z]+)?', r'person-\1'),
73
+ (r'AMOUNT_(\d+)(?:_[A-Z]+)?', r'amount-\1'),
74
+ (r'PERCENT_(\d+)(?:_[A-Z]+)?', r'percent-\1'),
75
+ (r'GROUP_(\d+)(?:_[A-Z]+)?', r'group-\1'),
76
+ (r'STOCK_SYMBOL_(\d+)(?:_[A-Z]+)?', r'stock-\1'),
77
+
78
+ # تبدیل حروف بزرگ به کوچک
79
+ (r'Company-(\d+)', r'company-\1'),
80
+ (r'Person-(\d+)', r'person-\1'),
81
+ (r'Amount-(\d+)', r'amount-\1'),
82
+ (r'Percent-(\d+)', r'percent-\1'),
83
+ (r'Group-(\d+)', r'group-\1'),
84
+ ]
85
+
86
+ normalized_text = text
87
+ for pattern, replacement in replacements:
88
+ normalized_text = re.sub(pattern, replacement, normalized_text)
89
+
90
+ return normalized_text
91
+
92
+ def calculate_precision_recall_f1_by_type(self, reference_counts: Dict[str, int],
93
+ predicted_counts: Dict[str, int]) -> Tuple[float, float, float]:
94
+ """محاسبه Precision, Recall و F1-Score بر اساس نوع موجودیت‌ها"""
95
+
96
+ # مجموع کل موجودیت‌ها
97
+ total_reference = sum(reference_counts.values())
98
+ total_predicted = sum(predicted_counts.values())
99
+
100
+ if total_predicted == 0 and total_reference == 0:
101
+ return 1.0, 1.0, 1.0 # هر دو خالی هستند - تطبیق کامل
102
+ elif total_predicted == 0:
103
+ return 0.0, 0.0, 0.0 # predicted خالی ولی reference دارد
104
+ elif total_reference == 0:
105
+ return 0.0, 1.0 if total_predicted > 0 else 1.0, 0.0 # reference خالی
106
+
107
+ # محاسبه True Positive برای هر نوع موجودیت
108
+ true_positives = 0
109
+ for entity_type in reference_counts.keys():
110
+ ref_count = reference_counts[entity_type]
111
+ pred_count = predicted_counts[entity_type]
112
+ # True Positive = کمترین تعداد بین reference و predicted برای هر نوع
113
+ true_positives += min(ref_count, pred_count)
114
+
115
+ # محاسبه Precision و Recall
116
+ precision = true_positives / total_predicted if total_predicted > 0 else 0.0
117
+ recall = true_positives / total_reference if total_reference > 0 else 0.0
118
+
119
+ # محاسبه F1-Score
120
+ if precision + recall == 0:
121
+ f1 = 0.0
122
+ else:
123
+ f1 = 2 * (precision * recall) / (precision + recall)
124
+
125
+ return precision, recall, f1
126
+
127
+ def evaluate_single_row(self, reference_text: str, predicted_text: str) -> Tuple[float, float, float]:
128
+ """ارزیابی یک سطر بر اساس نوع موجودیت‌ها"""
129
+ try:
130
+ # استخراج تعداد موجودیت‌ها بر اساس نوع
131
+ ref_counts = self.extract_entity_types_from_text(reference_text)
132
+ pred_counts = self.extract_entity_types_from_text(predicted_text)
133
+
134
+ # محاسبه متریک‌ها
135
+ precision, recall, f1 = self.calculate_precision_recall_f1_by_type(ref_counts, pred_counts)
136
+
137
+ return precision, recall, f1
138
+
139
+ except Exception as e:
140
+ print(f"خطا در ارزیابی: {str(e)}")
141
+ return 0.0, 0.0, 0.0
142
+
143
+ def debug_text_analysis(self, reference_text: str, predicted_text: str, row_num: int = 0) -> str:
144
+ """تابع debugging برای تحلیل متن‌ها بر اساس نوع موجودیت‌ها"""
145
+ debug_info = f"\n--- Debug Row {row_num + 1} ---\n"
146
+ debug_info += f"Reference: '{reference_text[:100]}...'\n"
147
+ debug_info += f"Predicted: '{predicted_text[:100]}...'\n"
148
+
149
+ ref_counts = self.extract_entity_types_from_text(reference_text)
150
+ pred_counts = self.extract_entity_types_from_text(predicted_text)
151
+
152
+ debug_info += f"Reference entity counts: {ref_counts}\n"
153
+ debug_info += f"Predicted entity counts: {pred_counts}\n"
154
+
155
+ # محاسبه متریک‌ها برای این سطر
156
+ precision, recall, f1 = self.calculate_precision_recall_f1_by_type(ref_counts, pred_counts)
157
+ debug_info += f"Metrics: P={precision:.3f}, R={recall:.3f}, F1={f1:.3f}\n"
158
+
159
+ return debug_info
160
+
161
+ def evaluate_dataset(self, file_path: str) -> Tuple[bool, str, pd.DataFrame]:
162
+ """ارزیابی کل دیتاست با پشتیبانی از فرمت‌های مختلف"""
163
+ try:
164
+ # بارگذاری فایل
165
+ df = pd.read_csv(file_path)
166
+
167
+ # بررسی ستون‌ها - پشتیبانی از دو حالت
168
+ if 'Reference_text' in df.columns and 'anonymized_text' in df.columns:
169
+ # حالت سه ستونه (فرمت قبلی)
170
+ required_columns = ['original_text', 'Reference_text', 'anonymized_text']
171
+ missing_columns = [col for col in required_columns if col not in df.columns]
172
+
173
+ if missing_columns:
174
+ return False, f"ستون‌های مفقود: {', '.join(missing_columns)}", pd.DataFrame()
175
+
176
+ reference_col = 'Reference_text'
177
+ predicted_col = 'anonymized_text'
178
+
179
+ elif 'original_text' in df.columns and 'anonymized_text' in df.columns:
180
+ # حالت دو ستونه جدید - فرض: original_text مرجع است
181
+ reference_col = 'original_text' # متن اصلی
182
+ predicted_col = 'anonymized_text' # متن ناشناس‌شده با regex
183
+
184
+ else:
185
+ return False, "فایل باید شامل ستون‌های 'original_text' و 'anonymized_text' باشد", pd.DataFrame()
186
+
187
+ # تشخیص مشکل - بررسی نمونه‌ای از داده‌ها
188
+ debug_info = "\n=== ارزیابی با پشتیبانی فرمت‌های مختلف ===\n"
189
+ debug_info += f"تعداد سطرها: {len(df)}\n"
190
+ debug_info += f"ستون‌ها: {list(df.columns)}\n"
191
+ debug_info += f"مرجع: {reference_col}, پیش‌بینی: {predicted_col}\n\n"
192
+
193
+ # بررسی چند سطر اول
194
+ for i in range(min(3, len(df))):
195
+ ref_text = str(df.iloc[i][reference_col])
196
+ pred_text = str(df.iloc[i][predicted_col])
197
+
198
+ debug_info += self.debug_text_analysis(ref_text, pred_text, i)
199
+
200
+ # نمایش نرمال‌سازی
201
+ normalized_ref = self.normalize_entity_format(ref_text)
202
+ normalized_pred = self.normalize_entity_format(pred_text)
203
+
204
+ if normalized_ref != ref_text or normalized_pred != pred_text:
205
+ debug_info += f" نرمال‌سازی مرجع: '{normalized_ref[:50]}...'\n"
206
+ debug_info += f" نرمال‌سازی پیش‌بینی: '{normalized_pred[:50]}...'\n"
207
+
208
+ print(debug_info) # نمایش در console
209
+
210
+ # محاسبه متریک‌ها برای هر سطر
211
+ precisions = []
212
+ recalls = []
213
+ f1_scores = []
214
+
215
+ total_entity_types_found = 0
216
+
217
+ for index, row in df.iterrows():
218
+ precision, recall, f1 = self.evaluate_single_row(
219
+ row[reference_col],
220
+ row[predicted_col]
221
+ )
222
+
223
+ precisions.append(round(precision, 4))
224
+ recalls.append(round(recall, 4))
225
+ f1_scores.append(round(f1, 4))
226
+
227
+ # شمارش انواع موجودیت‌ها برای debugging
228
+ ref_counts = self.extract_entity_types_from_text(str(row[reference_col]))
229
+ pred_counts = self.extract_entity_types_from_text(str(row[predicted_col]))
230
+ total_entity_types_found += sum(ref_counts.values()) + sum(pred_counts.values())
231
+
232
+ # اضافه کردن ستون‌های جدید
233
+ df['Precision'] = precisions
234
+ df['Recall'] = recalls
235
+ df['F1_Score'] = f1_scores
236
+
237
+ # ذخیره نتایج
238
+ self.results_df = df
239
+
240
+ # پیام وضعیت شامل اطلاعات debugging
241
+ avg_precision = np.mean(precisions)
242
+ avg_recall = np.mean(recalls)
243
+ avg_f1 = np.mean(f1_scores)
244
+
245
+ status_message = f"""ارزیابی انجام شد (مرجع: {reference_col}):
246
+ • میانگین Precision: {avg_precision:.3f}
247
+ • میانگین Recall: {avg_recall:.3f}
248
+ • میانگین F1-Score: {avg_f1:.3f}
249
+ • کل موجودیت‌های یافت شده: {total_entity_types_found}
250
+ • پشتیبانی فرمت REGEX اضافه شد"""
251
+
252
+ if total_entity_types_found == 0:
253
+ status_message += "\n⚠️ هیچ موجودیتی تشخیص داده نشد!"
254
+
255
+ return True, status_message, df
256
+
257
+ except Exception as e:
258
+ return False, f"خطا در پردازش فایل: {str(e)}", pd.DataFrame()
259
+
260
+ def generate_summary_report(self, df: pd.DataFrame) -> str:
261
+ """تولید گزارش خلاصه"""
262
+ if df.empty:
263
+ return "هیچ داده‌ای برای گزارش یافت نشد"
264
+
265
+ # محاسبه آمار کلی
266
+ avg_precision = df['Precision'].mean()
267
+ avg_recall = df['Recall'].mean()
268
+ avg_f1 = df['F1_Score'].mean()
269
+
270
+ # محاسبه آمار تفصیلی
271
+ total_rows = len(df)
272
+ high_precision_count = len(df[df['Precision'] >= 0.8])
273
+ high_recall_count = len(df[df['Recall'] >= 0.8])
274
+ high_f1_count = len(df[df['F1_Score'] >= 0.8])
275
+
276
+ # بهترین و بدترین نتایج
277
+ best_f1_idx = df['F1_Score'].idxmax()
278
+ worst_f1_idx = df['F1_Score'].idxmin()
279
+
280
+ report = f"""
281
+ ## 📊 گزارش جامع ارزیابی
282
+
283
+ ### آمار کلی:
284
+ - **تعداد کل سطرها:** {total_rows}
285
+ - **میانگین Precision:** {avg_precision:.4f}
286
+ - **میانگین Recall:** {avg_recall:.4f}
287
+ - **میانگین F1-Score:** {avg_f1:.4f}
288
+
289
+ ### توزیع عملکرد (امتیاز ≥ 0.8):
290
+ - **Precision بالا:** {high_precision_count} سطر ({high_precision_count/total_rows*100:.1f}%)
291
+ - **Recall بالا:** {high_recall_count} سطر ({high_recall_count/total_rows*100:.1f}%)
292
+ - **F1-Score بالا:** {high_f1_count} سطر ({high_f1_count/total_rows*100:.1f}%)
293
+
294
+ ### نمونه‌های برتر و ضعیف:
295
+ - **بهترین F1-Score:** {df.loc[best_f1_idx, 'F1_Score']:.4f} (سطر {best_f1_idx + 1})
296
+ - **ضعیف‌ترین F1-Score:** {df.loc[worst_f1_idx, 'F1_Score']:.4f} (سطر {worst_f1_idx + 1})
297
+ """
298
+
299
+ return report
300
+
301
+ def create_downloadable_csv(self) -> bytes:
302
+ """ایجاد محتوای CSV برای دانلود مستقیم"""
303
+ if self.results_df is None or self.results_df.empty:
304
+ return None
305
+
306
+ try:
307
+ # تولید محتوای CSV در حافظه
308
+ csv_buffer = io.StringIO()
309
+ self.results_df.to_csv(csv_buffer, index=False, encoding='utf-8')
310
+ csv_content = csv_buffer.getvalue()
311
+ csv_buffer.close()
312
+
313
+ # تبدیل به bytes برای دانلود
314
+ return csv_content.encode('utf-8-sig')
315
+
316
+ except Exception as e:
317
+ print(f"خطا در ایجاد محتوای CSV: {str(e)}")
318
+ return None
319
+
320
+ def create_evaluation_interface():
321
+ """ایجاد رابط کاربری ارزیابی"""
322
+ evaluator = AnonymizationEvaluator()
323
+
324
+ with gr.Blocks(
325
+ title="ارزیابی ناشناس‌سازی",
326
+ theme=gr.themes.Soft(),
327
+ css="""
328
+ .gradio-container {
329
+ font-family: 'Tahoma', 'Arial', sans-serif !important;
330
+ direction: rtl;
331
+ max-width: 1200px;
332
+ margin: 0 auto;
333
+ }
334
+ .upload-area {
335
+ border: 2px dashed #4CAF50;
336
+ border-radius: 15px;
337
+ padding: 30px;
338
+ text-align: center;
339
+ background: linear-gradient(145deg, #f8f9fa, #e9ecef);
340
+ margin: 20px 0;
341
+ }
342
+ .results-table {
343
+ direction: ltr;
344
+ font-family: monospace;
345
+ font-size: 12px;
346
+ }
347
+ .summary-box {
348
+ background-color: #e3f2fd;
349
+ border: 1px solid #2196F3;
350
+ border-radius: 10px;
351
+ padding: 20px;
352
+ margin: 15px 0;
353
+ }
354
+ """
355
+ ) as interface:
356
+
357
+ gr.Markdown("""
358
+ # 📊 ابزار ارزیابی ناشناس‌سازی با پشتیبانی فرمت‌های مختلف
359
+ ### پشتیبانی از فرمت‌های استاندارد و REGEX
360
+ """)
361
+
362
+ with gr.Row():
363
+ with gr.Column(scale=1):
364
+ gr.Markdown("### 📁 بارگذاری فایل")
365
+
366
+ file_input = gr.File(
367
+ label="انتخاب فایل CSV (2 یا 3 ستون)",
368
+ file_types=[".csv"],
369
+ elem_classes=["upload-area"]
370
+ )
371
+
372
+ evaluate_btn = gr.Button(
373
+ "🚀 شروع ارزیابی",
374
+ variant="primary",
375
+ size="lg",
376
+ interactive=False
377
+ )
378
+
379
+ download_btn = gr.Button(
380
+ "💾 دانلود نتایج CSV",
381
+ variant="secondary",
382
+ visible=False
383
+ )
384
+
385
+ with gr.Column(scale=2):
386
+ status_output = gr.Markdown("وضعیت: آماده بارگذاری فایل...")
387
+
388
+ summary_output = gr.Markdown(
389
+ visible=False,
390
+ elem_classes=["summary-box"]
391
+ )
392
+
393
+ # جدول نتایج
394
+ results_table = gr.Dataframe(
395
+ label="نتایج تفصیلی (نمایش 10 سطر اول)",
396
+ visible=False,
397
+ elem_classes=["results-table"],
398
+ wrap=True
399
+ )
400
+
401
+ # فایل دانلود
402
+ download_file = gr.File(
403
+ visible=False,
404
+ label="فایل نتایج"
405
+ )
406
+
407
+ def on_file_upload(file):
408
+ if file is None:
409
+ return "❌ لطفاً فایل را انتخاب کنید", gr.Button(interactive=False)
410
+
411
+ return "✅ فایل بارگذاری شد، آماده ارزیابی", gr.Button(interactive=True)
412
+
413
+ def evaluate_file(file):
414
+ if file is None:
415
+ return (
416
+ "❌ هیچ فایلی انتخاب نشده",
417
+ gr.Markdown(visible=False),
418
+ gr.Dataframe(visible=False),
419
+ gr.Button(visible=False),
420
+ gr.File(visible=False)
421
+ )
422
+
423
+ try:
424
+ success, message, df = evaluator.evaluate_dataset(file.name)
425
+
426
+ if not success:
427
+ return (
428
+ f"❌ {message}",
429
+ gr.Markdown(visible=False),
430
+ gr.Dataframe(visible=False),
431
+ gr.Button(visible=False),
432
+ gr.File(visible=False)
433
+ )
434
+
435
+ # تولید گزارش خلاصه
436
+ summary = evaluator.generate_summary_report(df)
437
+
438
+ # نمایش 10 سطر اول برای نمونه در رابط
439
+ display_df = df.head(10)
440
+
441
+ # پیام اطلاع‌رسانی
442
+ status_message = f"✅ {message} - {len(df)} سطر پردازش شد. نمایش: 10 سطر اول، دانلود: همه سطرها"
443
+
444
+ return (
445
+ status_message,
446
+ gr.Markdown(value=summary, visible=True),
447
+ gr.Dataframe(value=display_df, visible=True),
448
+ gr.Button(visible=True),
449
+ gr.File(visible=False)
450
+ )
451
+
452
+ except Exception as e:
453
+ return (
454
+ f"❌ خطای غیرمنتظره: {str(e)}",
455
+ gr.Markdown(visible=False),
456
+ gr.Dataframe(visible=False),
457
+ gr.Button(visible=False),
458
+ gr.File(visible=False)
459
+ )
460
+
461
+ def download_results():
462
+ try:
463
+ if evaluator.results_df is None or evaluator.results_df.empty:
464
+ return (
465
+ "❌ هیچ داده‌ای برای دانلود وجود ندارد. ابتدا ارزیابی را انجام دهید.",
466
+ gr.File(visible=False)
467
+ )
468
+
469
+ # ایجاد محتوای CSV
470
+ csv_content = evaluator.create_downloadable_csv()
471
+ if csv_content:
472
+ # تولید نام فایل
473
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
474
+ filename = f"evaluation_results_{timestamp}.csv"
475
+
476
+ # ذخیره در فایل موقت برای دانلود
477
+ with tempfile.NamedTemporaryFile(mode='wb', delete=False,
478
+ suffix='.csv', prefix='eval_') as temp_file:
479
+ temp_file.write(csv_content)
480
+ temp_filename = temp_file.name
481
+
482
+ return (
483
+ f"✅ فایل نتایج آماده شد: {filename} ({len(evaluator.results_df)} سطر)",
484
+ gr.File(value=temp_filename, visible=True)
485
+ )
486
+ else:
487
+ return (
488
+ "❌ خطا در ایجاد محتوای CSV",
489
+ gr.File(visible=False)
490
+ )
491
+ except Exception as e:
492
+ return (
493
+ f"❌ خطا در دانلود: {str(e)}",
494
+ gr.File(visible=False)
495
+ )
496
+
497
+ # اتصال رویدادها
498
+ file_input.change(
499
+ fn=on_file_upload,
500
+ inputs=[file_input],
501
+ outputs=[status_output, evaluate_btn]
502
+ )
503
+
504
+ evaluate_btn.click(
505
+ fn=evaluate_file,
506
+ inputs=[file_input],
507
+ outputs=[status_output, summary_output, results_table, download_btn, download_file]
508
+ )
509
+
510
+ download_btn.click(
511
+ fn=download_results,
512
+ outputs=[status_output, download_file]
513
+ )
514
+
515
+ # راهنمای استفاده
516
+ with gr.Accordion("📖 راهنمای استفاده", open=False):
517
+ gr.Markdown("""
518
+ ### فرمت‌های پشتیبانی شده:
519
+
520
+ **حالت 1: فایل 3 ستونه (مرجع و پیش‌بینی جداگانه)**
521
+ - original_text: متن اصلی
522
+ - Reference_text: متن مرجع ناشناس‌شده
523
+ - anonymized_text: متن پیش‌بینی شده
524
+
525
+ **حالت 2: فایل 2 ستونه (مقایسه با اصل)**
526
+ - original_text: متن اصلی (مرجع)
527
+ - anonymized_text: متن ناشناس‌شده (پیش‌بینی)
528
+
529
+ ### انواع فرمت‌های موجودیت پشتیبانی شده:
530
+
531
+ **فرمت استاندارد:**
532
+ - company-01, person-02, amount-03
533
+ - Company-01, Person-02, Amount-03
534
+ - COMPANY-01, PERSON-02, AMOUNT-03
535
+
536
+ **فرمت REGEX:**
537
+ - COMPANY_001_REGEX, PERSON_002_REGEX
538
+ - AMOUNT_012, PERCENT_025
539
+ - STOCK_SYMBOL_001_REGEX
540
+
541
+ ### نرمال‌سازی خودکار:
542
+
543
+ برنامه خودکار انواع مختلف را تشخیص داده و نرمال‌سازی می‌کند:
544
+ - COMPANY_001_REGEX → company-001
545
+ - AMOUNT_012 → amount-012
546
+ - STOCK_SYMBOL_005_REGEX → stock-005
547
+
548
+ ### متریک‌های محاسبه شده:
549
+
550
+ - **Precision**: موجودیت‌های درست / کل شناسایی‌شده
551
+ - **Recall**: موجودیت‌های درست / کل مرجع
552
+ - **F1-Score**: میانگین هارمونیک Precision و Recall
553
+
554
+ ### مراحل کار:
555
+
556
+ 1. فایل CSV را آپلود کنید (2 یا 3 ستون)
557
+ 2. روی "شروع ارزیابی" کلیک کنید
558
+ 3. گزارش و نتایج را مشاهده کنید
559
+ 4. فایل نتایج کامل را دانلود کنید
560
+
561
+ ### نکات:
562
+
563
+ - برنامه خودکار تشخیص می‌دهد فایل 2 ستونه است یا 3 ستونه
564
+ - فرمت‌های مختلف خودکار نرمال‌سازی می‌شوند
565
+ - فقط نوع و تعداد موجودیت‌ها مهم است، شناسه عددی نه
566
+ """)
567
+
568
+ # نمایش مثال فرمت‌های مختلف
569
+ with gr.Accordion("💡 مثال فرمت‌های مختلف", open=False):
570
+ gr.Markdown("""
571
+ ### مثال 1: فرمت استاندارد vs فرمت REGEX
572
+
573
+ **متن اصلی**: `شرکت فولاد مبارکه با درآمد 127 میلیارد تومان`
574
+
575
+ **فرمت استاندارد**: `شرکت company-01 با درآمد amount-02 میلیارد تومان`
576
+
577
+ **فرمت REGEX**: `شرکت COMPANY_001_REGEX با درآمد AMOUNT_012 میلیارد تومان`
578
+
579
+ **نتیجه ارزیابی**:
580
+ - هر دو: 1 company + 1 amount
581
+ - Precision = 1.0, Recall = 1.0, F1 = 1.0
582
+
583
+ ### مثال 2: تعداد موجودیت‌های متفاوت
584
+
585
+ **مرجع**: `company-01 amount-02 person-03` (1+1+1=3)
586
+
587
+ **پیش‌بینی**: `COMPANY_001_REGEX AMOUNT_012` (1+1=2)
588
+
589
+ **نتیجه**:
590
+ - True Positive = min(1,1) + min(1,1) + min(1,0) = 2
591
+ - Precision = 2/2 = 1.0
592
+ - Recall = 2/3 = 0.67
593
+ - F1 = 0.80
594
+ """)
595
+
596
+
597
+ return interface
598
+
599
+ # اجرای برنامه
600
+ if __name__ == "__main__":
601
+ interface = create_evaluation_interface()
602
+ interface.launch()