leilaghomashchi commited on
Commit
a5fd5e3
·
verified ·
1 Parent(s): 27ac874

Upload app_anonymizer_v2 (1).py

Browse files
Files changed (1) hide show
  1. app_anonymizer_v2 (1).py +588 -0
app_anonymizer_v2 (1).py ADDED
@@ -0,0 +1,588 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ سیستم ناشناس‌سازی متون فارسی با پرامپت بهبود یافته
3
+ بر اساس تحلیل 340 نمونه بنچمارک - نسخه 2.0
4
+ """
5
+
6
+ import requests
7
+ import json
8
+ import gradio as gr
9
+ from typing import Dict, Any, List, Generator
10
+ import os
11
+ from dataclasses import dataclass
12
+ import re
13
+ import pandas as pd
14
+ import time
15
+ from datetime import datetime
16
+ import threading
17
+
18
+ # ============================================
19
+ # پرامپت بهبود یافته
20
+ # ============================================
21
+
22
+ IMPROVED_SYSTEM_PROMPT = """شما یک «ناشناس‌ساز متون مالی/خبری فارسی» هستید. وظیفه‌تان جایگزینی اسامی خاص و مقادیر عددی با شناسه‌های بی‌معناست.
23
+
24
+ ## **قوانین اندیس‌گذاری - CRITICAL**
25
+ ### **1. ترتیب شماره‌گذاری الزامی:**
26
+ - شرکت‌ها: company-01, company-02, company-03, ... (پیوسته و بدون گپ)
27
+ - اشخاص: person-01, person-02, person-03, ... (پیوسته و بدون گپ)
28
+ - اعداد/مبالغ: amount-01, amount-02, amount-03, ... (پیوسته و بدون گپ)
29
+ - درصدها: percent-01, percent-02, percent-03, ... (پیوسته و بدون گپ)
30
+
31
+ ### **2. ثبات شناسه‌ها در متن:**
32
+ - اگر "همراه اول" اول‌بار company-01 شد، در تمام متن همان باشد
33
+
34
+ ## **⚠️ قوانین حیاتی برای واحدها و مبالغ:**
35
+
36
+ ### **قانون 1: مبالغ کامل را یکجا جایگزین کن (بدون واحد)**
37
+ - "23 هزار و 296 میلیارد تومان" → `amount-01` ✅
38
+ - "23 هزار و 296 میلیارد تومان" → `amount-01 تومان` ❌
39
+ - "500 میلیون دلار" → `amount-01` ✅
40
+ - "681,667 میلیارد ریال" → `amount-01` ✅
41
+
42
+ ### **قانون 2: پسوندهای صفتی (-ی) را حفظ کن**
43
+ - "155 هزار میلیارد ریالی" → `amount-01 ریالی` ✅
44
+ - "2700 میلیارد تومانی" → `amount-01 تومانی` ✅
45
+
46
+ ### **قانون 3: کلمه "درصد" را حذف کن**
47
+ - "4.58 درصد" → `percent-01` ✅
48
+ - "4.58 درصد" → `percent-01 درصد` ❌
49
+ - "37 درصدی" → `percent-01` ✅
50
+
51
+ ## **⚠️ موارد حفظ شده (CRITICAL):**
52
+
53
+ ### **1. سامانه کدال - حفظ شود!**
54
+ - "سامانه کدال" → `سامانه کدال` ✅ (تغییر نکند!)
55
+ - "سامانه کدال" → `company-XX` ❌ (اشتباه!)
56
+
57
+ ### **2. تاریخ‌ها و سال‌ها**
58
+ - "سال 1402" → `سال 1402` ✅
59
+ - "1404/04/29" → `1404/04/29` ✅
60
+ - "پاییز ۱۴۰۱" → `پاییز ۱۴۰۱` ✅
61
+
62
+ ### **3. دوره‌های زمانی**
63
+ - "۵ ماهه سال"، "سه‌ماهه نخست"، "۹ ماهه" → حفظ شوند ✅
64
+
65
+ ### **4. کلمات عمومی بدون نام خاص**
66
+ - "سه شرکت دارویی"، "چند بانک"، "12 بانک کشور" → حفظ شوند ✅
67
+
68
+ ## **تشخیص صحیح انواع موجودیت‌ها:**
69
+
70
+ ### **شرکت/سازمان (company-XX):**
71
+ - نام‌های خاص شرکت: ایران خودرو، بانک ملی، همراه اول
72
+ - سازمان‌های دولتی: سازمان تامین اجتماعی، وزارت نفت
73
+ - گروه‌ها: "گروه همراه اول" → company-XX ✅
74
+ - بازرس/حسابرس: "شرکت وانیا نیک تدبیر" → company-XX ✅
75
+
76
+ ### **شخص (person-XX):**
77
+ - نام و نام‌خانوادگی: مهدی اخوان بهابادی، فرج‌اله قدمی
78
+
79
+ ### **مبلغ/عدد (amount-XX):**
80
+ - مبالغ مالی، تعداد، اعداد (⚠️ سال‌ها amount نیستند!)
81
+
82
+ ### **درصد (percent-XX):**
83
+ - "4.58 درصد"، "37 درصدی" → percent-XX (بدون کلمه درصد)
84
+
85
+ ## **مثال‌های صحیح:**
86
+
87
+ **مثال 1:**
88
+ ورودی: ایران خودرو در اسفندماه سال 1402 حدود 23 هزار و 296 میلیارد تومان درآمد کسب کرد که در مقایسه با بهمن 4.58 درصد افزایش داشت.
89
+ خروجی: company-01 در اسفندماه سال 1402 حدود amount-01 درآمد کسب کرد که در مقایسه با بهمن percent-01 افزایش داشت.
90
+
91
+ **مثال 2:**
92
+ ورودی: بانک پاسارگاد با شناسایی سود خالص 155 هزار میلیارد ریالی در رده دوم قرار گرفت.
93
+ خروجی: company-01 با شناسایی سود خالص amount-01 ریالی در رده دوم قرار گرفت.
94
+
95
+ **مثال 3:**
96
+ ورودی: شرکت تیپیکو گزارش خود را در سامانه کدال منتشر کرد.
97
+ خروجی: company-01 گزارش خود را در سامانه کدال منتشر کرد.
98
+
99
+ **مثال 4:**
100
+ ورودی: رشد 14 درصدی سرمایه‌گذاری‌ها به 5000 میلیارد تومان رسید.
101
+ خروجی: رشد percent-01 سرمایه‌گذاری‌ها به amount-01 رسید.
102
+
103
+ **مثال 5:**
104
+ ورودی: زیان خالص 2700 میلیارد تومانی در سه‌ماهه نخست 1404 گزارش کرد.
105
+ خروجی: زیان خالص amount-01 تومانی در سه‌ماهه نخست 1404 گزارش کرد.
106
+
107
+ **مثال 6:**
108
+ ورودی: سازمان تامین اجتماعی دارای سه شرکت دارویی است.
109
+ خروجی: company-01 دارای سه شرکت دارویی است.
110
+
111
+ ## **خلاصه قوانین:**
112
+ 1. مبالغ کامل → amount-XX (بدون واحد: تومان، ریال، دلار، همت)
113
+ 2. پسوند صفتی (-ی) → حفظ شود (ریالی، تومانی)
114
+ 3. درصد/درصدی → percent-XX (بدون کلمه درصد)
115
+ 4. سامانه کدال → حفظ شود (company نشود)
116
+ 5. سال‌ها/تاریخ‌ها → حفظ شوند
117
+ 6. کلمات عمومی → حفظ شوند
118
+ 7. گروه‌ها → company-XX
119
+
120
+ **فقط متن ناشناس‌شده را برگردان - هیچ توضیح اضافی نیاز نیست.**
121
+ """
122
+
123
+ # ============================================
124
+ # تنظیمات
125
+ # ============================================
126
+
127
+ @dataclass
128
+ class CerebrasConfig:
129
+ """تنظیمات Cerebras API"""
130
+ api_key: str
131
+ base_url: str = "https://api.cerebras.ai/v1"
132
+ model: str = "llama-3.3-70b"
133
+ max_tokens: int = 2000
134
+ temperature: float = 0.1
135
+
136
+ @dataclass
137
+ class RateLimitConfig:
138
+ """تنظیمات محدودیت نرخ درخواست"""
139
+ requests_per_minute: int = 30
140
+ min_delay_between_requests: float = 2.5
141
+ max_retries: int = 5
142
+ initial_backoff: float = 5.0
143
+ max_backoff: float = 120.0
144
+ backoff_multiplier: float = 2.0
145
+
146
+ # ============================================
147
+ # Rate Limiter
148
+ # ============================================
149
+
150
+ class RateLimiter:
151
+ """مدیریت محدودیت نرخ درخواست"""
152
+
153
+ def __init__(self, config: RateLimitConfig):
154
+ self.config = config
155
+ self.request_times: List[float] = []
156
+ self.lock = threading.Lock()
157
+ self.consecutive_failures = 0
158
+
159
+ def wait_if_needed(self) -> float:
160
+ with self.lock:
161
+ now = time.time()
162
+ self.request_times = [t for t in self.request_times if now - t < 60]
163
+
164
+ wait_time = 0.0
165
+
166
+ if len(self.request_times) >= self.config.requests_per_minute:
167
+ oldest_request = min(self.request_times)
168
+ wait_time = max(wait_time, 60 - (now - oldest_request) + 1)
169
+
170
+ if self.request_times:
171
+ time_since_last = now - max(self.request_times)
172
+ if time_since_last < self.config.min_delay_between_requests:
173
+ wait_time = max(wait_time, self.config.min_delay_between_requests - time_since_last)
174
+
175
+ if self.consecutive_failures > 0:
176
+ failure_wait = min(
177
+ self.config.initial_backoff * (self.config.backoff_multiplier ** self.consecutive_failures),
178
+ self.config.max_backoff
179
+ )
180
+ wait_time = max(wait_time, failure_wait)
181
+
182
+ if wait_time > 0:
183
+ time.sleep(wait_time)
184
+
185
+ self.request_times.append(time.time())
186
+ return wait_time
187
+
188
+ def report_success(self):
189
+ with self.lock:
190
+ self.consecutive_failures = 0
191
+
192
+ def report_failure(self, is_rate_limit: bool = False):
193
+ with self.lock:
194
+ if is_rate_limit:
195
+ self.consecutive_failures += 1
196
+ else:
197
+ self.consecutive_failures = min(self.consecutive_failures + 0.5, 3)
198
+
199
+ # ============================================
200
+ # Anonymizer با پرامپت بهبود یافته
201
+ # ============================================
202
+
203
+ class ImprovedCerebrasAnonymizer:
204
+ """سیستم ناشناس‌سازی با پرامپت بهبود یافته"""
205
+
206
+ def __init__(self, api_key: str = None, rate_limit_config: RateLimitConfig = None):
207
+ if api_key is None:
208
+ api_key = os.getenv("CEREBRAS_API_KEY")
209
+ if not api_key:
210
+ raise ValueError("کلید API یافت نشد")
211
+
212
+ self.config = CerebrasConfig(api_key=api_key)
213
+ self.rate_limit_config = rate_limit_config or RateLimitConfig()
214
+ self.rate_limiter = RateLimiter(self.rate_limit_config)
215
+ self.system_prompt = IMPROVED_SYSTEM_PROMPT
216
+
217
+ def _make_api_request_with_retry(self, text: str) -> Dict[str, Any]:
218
+ """ارسال درخواست با مدیریت retry"""
219
+ headers = {
220
+ "Authorization": f"Bearer {self.config.api_key}",
221
+ "Content-Type": "application/json"
222
+ }
223
+
224
+ payload = {
225
+ "messages": [
226
+ {"role": "system", "content": self.system_prompt},
227
+ {"role": "user", "content": text}
228
+ ],
229
+ "model": self.config.model,
230
+ "temperature": self.config.temperature,
231
+ "max_tokens": self.config.max_tokens
232
+ }
233
+
234
+ last_error = None
235
+
236
+ for attempt in range(self.rate_limit_config.max_retries):
237
+ self.rate_limiter.wait_if_needed()
238
+
239
+ try:
240
+ response = requests.post(
241
+ f"{self.config.base_url}/chat/completions",
242
+ headers=headers,
243
+ json=payload,
244
+ timeout=60
245
+ )
246
+
247
+ if response.status_code == 429:
248
+ self.rate_limiter.report_failure(is_rate_limit=True)
249
+ retry_after = response.headers.get('Retry-After')
250
+ wait_seconds = int(retry_after) if retry_after else min(
251
+ self.rate_limit_config.initial_backoff * (self.rate_limit_config.backoff_multiplier ** attempt),
252
+ self.rate_limit_config.max_backoff
253
+ )
254
+ last_error = f"Rate limit (429). تلاش {attempt + 1}/{self.rate_limit_config.max_retries}"
255
+ time.sleep(wait_seconds)
256
+ continue
257
+
258
+ response.raise_for_status()
259
+ self.rate_limiter.report_success()
260
+ return response.json()
261
+
262
+ except requests.exceptions.Timeout:
263
+ self.rate_limiter.report_failure(is_rate_limit=False)
264
+ last_error = f"Timeout. تلاش {attempt + 1}/{self.rate_limit_config.max_retries}"
265
+ time.sleep(self.rate_limit_config.initial_backoff)
266
+
267
+ except requests.exceptions.RequestException as e:
268
+ self.rate_limiter.report_failure(is_rate_limit=False)
269
+ last_error = f"خطا: {str(e)}"
270
+ time.sleep(self.rate_limit_config.initial_backoff)
271
+
272
+ raise Exception(f"ناموفق پس از {self.rate_limit_config.max_retries} تلاش: {last_error}")
273
+
274
+ def anonymize_text(self, text: str) -> Dict[str, Any]:
275
+ """ناشناس‌سازی متن"""
276
+ if not text or not text.strip():
277
+ return {"success": False, "error": "متن خالی", "anonymized_text": ""}
278
+
279
+ try:
280
+ response = self._make_api_request_with_retry(text)
281
+
282
+ if "choices" not in response or not response["choices"]:
283
+ return {"success": False, "error": "پاسخ نامعتبر", "anonymized_text": ""}
284
+
285
+ content = response["choices"][0]["message"]["content"]
286
+ content = self._clean_markdown(content).strip()
287
+
288
+ analysis = self._analyze_anonymized_text(content)
289
+
290
+ return {
291
+ "success": True,
292
+ "anonymized_text": content,
293
+ "entities": analysis["entities"],
294
+ "statistics": analysis["statistics"],
295
+ "usage": response.get("usage", {})
296
+ }
297
+
298
+ except Exception as e:
299
+ return {"success": False, "error": str(e), "anonymized_text": ""}
300
+
301
+ def _clean_markdown(self, content: str) -> str:
302
+ if "```" in content:
303
+ lines = content.split('\n')
304
+ clean_lines = []
305
+ skip = False
306
+ for line in lines:
307
+ if line.strip().startswith('```'):
308
+ skip = not skip
309
+ continue
310
+ if not skip:
311
+ clean_lines.append(line)
312
+ content = '\n'.join(clean_lines)
313
+ return content
314
+
315
+ def _analyze_anonymized_text(self, text: str) -> Dict[str, Any]:
316
+ companies = re.findall(r'company-(\d+)', text)
317
+ persons = re.findall(r'person-(\d+)', text)
318
+ amounts = re.findall(r'amount-(\d+)', text)
319
+ percents = re.findall(r'percent-(\d+)', text)
320
+
321
+ statistics = {
322
+ "company": len(set(companies)),
323
+ "person": len(set(persons)),
324
+ "amount": len(set(amounts)),
325
+ "percent": len(set(percents)),
326
+ "total": len(companies) + len(persons) + len(amounts) + len(percents)
327
+ }
328
+
329
+ entities = {
330
+ "companies": sorted(list(set(companies)), key=lambda x: int(x)),
331
+ "persons": sorted(list(set(persons)), key=lambda x: int(x)),
332
+ "amounts": sorted(list(set(amounts)), key=lambda x: int(x)),
333
+ "percents": sorted(list(set(percents)), key=lambda x: int(x))
334
+ }
335
+
336
+ return {"statistics": statistics, "entities": entities}
337
+
338
+ # ============================================
339
+ # Batch Processor
340
+ # ============================================
341
+
342
+ class BatchProcessor:
343
+ """پردازشگر دسته‌ای"""
344
+
345
+ def __init__(self, api_key: str, rate_limit_config: RateLimitConfig = None):
346
+ self.api_key = api_key
347
+ self.rate_limit_config = rate_limit_config or RateLimitConfig()
348
+ self.anonymizer = None
349
+ self.is_cancelled = False
350
+ self.processed_rows = 0
351
+ self.failed_rows = 0
352
+ self.start_time = None
353
+
354
+ def cancel(self):
355
+ self.is_cancelled = True
356
+
357
+ def reset(self):
358
+ self.is_cancelled = False
359
+ self.processed_rows = 0
360
+ self.failed_rows = 0
361
+ self.start_time = None
362
+
363
+ def process_csv(self, file_path: str, text_column: str, output_column: str = "anonymized_text"):
364
+ self.reset()
365
+ self.start_time = time.time()
366
+
367
+ # خواندن CSV
368
+ try:
369
+ df = pd.read_csv(file_path, encoding='utf-8')
370
+ except:
371
+ try:
372
+ df = pd.read_csv(file_path, encoding='utf-8-sig')
373
+ except:
374
+ df = pd.read_csv(file_path, encoding='cp1256')
375
+
376
+ if text_column not in df.columns:
377
+ yield {"type": "error", "message": f"ستون '{text_column}' یافت نشد"}
378
+ return
379
+
380
+ total_rows = len(df)
381
+ self.anonymizer = ImprovedCerebrasAnonymizer(
382
+ api_key=self.api_key,
383
+ rate_limit_config=self.rate_limit_config
384
+ )
385
+
386
+ df[output_column] = ""
387
+ df["status"] = ""
388
+
389
+ yield {"type": "info", "message": f"🚀 شروع پردازش {total_rows} ردیف..."}
390
+
391
+ for idx, row in df.iterrows():
392
+ if self.is_cancelled:
393
+ yield {"type": "cancelled", "processed": self.processed_rows}
394
+ break
395
+
396
+ text = str(row[text_column]) if pd.notna(row[text_column]) else ""
397
+
398
+ if not text.strip():
399
+ df.at[idx, output_column] = ""
400
+ df.at[idx, "status"] = "خالی"
401
+ self.processed_rows += 1
402
+ continue
403
+
404
+ result = self.anonymizer.anonymize_text(text)
405
+
406
+ if result["success"]:
407
+ df.at[idx, output_column] = result["anonymized_text"]
408
+ df.at[idx, "status"] = "✅"
409
+ self.processed_rows += 1
410
+ else:
411
+ df.at[idx, output_column] = f"خطا: {result.get('error', '')}"
412
+ df.at[idx, "status"] = "❌"
413
+ self.failed_rows += 1
414
+
415
+ progress = (idx + 1) / total_rows * 100
416
+ elapsed = time.time() - self.start_time
417
+
418
+ yield {
419
+ "type": "progress",
420
+ "current": idx + 1,
421
+ "total": total_rows,
422
+ "progress": progress,
423
+ "processed": self.processed_rows,
424
+ "failed": self.failed_rows,
425
+ "elapsed": elapsed
426
+ }
427
+
428
+ if not self.is_cancelled:
429
+ output_path = file_path.replace('.csv', '_anonymized_v2.csv')
430
+ df.to_csv(output_path, index=False, encoding='utf-8-sig')
431
+
432
+ yield {
433
+ "type": "complete",
434
+ "output_path": output_path,
435
+ "total": total_rows,
436
+ "processed": self.processed_rows,
437
+ "failed": self.failed_rows,
438
+ "time": time.time() - self.start_time,
439
+ "dataframe": df
440
+ }
441
+
442
+ # ============================================
443
+ # رابط کاربری Gradio
444
+ # ============================================
445
+
446
+ def create_interface():
447
+ """ایجاد رابط کاربری"""
448
+
449
+ api_key_available = bool(os.getenv("CEREBRAS_API_KEY"))
450
+ batch_processor = {"instance": None}
451
+
452
+ css = """
453
+ .gradio-container { direction: rtl; font-family: Tahoma, Arial; }
454
+ .success-box { background: #d4edda; padding: 15px; border-radius: 10px; color: #155724; }
455
+ .warning-box { background: #fff3cd; padding: 15px; border-radius: 10px; color: #856404; }
456
+ .info-box { background: #d1ecf1; padding: 15px; border-radius: 10px; color: #0c5460; }
457
+ """
458
+
459
+ with gr.Blocks(css=css, title="ناشناس‌ساز بهبود یافته v2.0", theme=gr.themes.Soft()) as interface:
460
+
461
+ gr.Markdown("""
462
+ # 🔒 سیستم ناشناس‌سازی متون فارسی - نسخه بهبود یافته 2.0
463
+ ### ⚡ با پرامپت بهینه‌شده بر اساس تحلیل 340 نمونه بنچمارک
464
+ """)
465
+
466
+ gr.Markdown("""
467
+ <div class="info-box">
468
+ 📌 <strong>بهبودهای نسخه 2.0:</strong><br>
469
+ • حذف واحدها از مبالغ (تومان، ریال، دلار → amount-XX)<br>
470
+ • حفظ پسوندهای صفتی (ریالی، تومانی)<br>
471
+ • حذف کلمه "درصد" (37 درصد → percent-01)<br>
472
+ • حفظ "سامانه کدال" (company نمی‌شود)<br>
473
+ • حفظ سال‌ها و تاریخ‌ها
474
+ </div>
475
+ """)
476
+
477
+ with gr.Tabs():
478
+ # تب پردازش تکی
479
+ with gr.Tab("📝 پردازش تکی"):
480
+ if not api_key_available:
481
+ api_key = gr.Textbox(label="🔑 کلید API", type="password")
482
+ else:
483
+ api_key = gr.Textbox(visible=False, value="")
484
+
485
+ with gr.Row():
486
+ input_text = gr.Textbox(label="📝 متن ورودی", lines=8)
487
+ output_text = gr.Textbox(label="🎯 متن ناشناس‌شده", lines=8)
488
+
489
+ process_btn = gr.Button("🔒 ناشناس‌سازی", variant="primary")
490
+ stats_output = gr.Markdown()
491
+
492
+ # تب پردازش دسته‌ای
493
+ with gr.Tab("📁 پردازش دسته‌ای CSV"):
494
+ if not api_key_available:
495
+ batch_api_key = gr.Textbox(label="🔑 کلید API", type="password")
496
+ else:
497
+ batch_api_key = gr.Textbox(visible=False, value="")
498
+
499
+ csv_file = gr.File(label="📂 فایل CSV", file_types=[".csv"])
500
+
501
+ with gr.Row():
502
+ text_column = gr.Dropdown(label="📑 ستون متن", choices=[], interactive=True)
503
+ delay_slider = gr.Slider(1, 10, value=2.5, label="⏱️ تأخیر (ثانیه)")
504
+
505
+ with gr.Row():
506
+ start_btn = gr.Button("🚀 شروع", variant="primary")
507
+ cancel_btn = gr.Button("⏹️ لغو", variant="stop")
508
+
509
+ progress_bar = gr.Slider(0, 100, value=0, label="📊 پیشرفت", interactive=False)
510
+ progress_text = gr.Markdown("در انتظار...")
511
+ output_file = gr.File(label="📥 دانلود", visible=False)
512
+
513
+ # توابع
514
+ def process_single(text, key):
515
+ if not text.strip():
516
+ return "", "⚠️ متن خالی"
517
+
518
+ api = key if key else os.getenv("CEREBRAS_API_KEY")
519
+ if not api:
520
+ return "", "❌ کلید API وارد نشده"
521
+
522
+ try:
523
+ anonymizer = ImprovedCerebrasAnonymizer(api_key=api)
524
+ result = anonymizer.anonymize_text(text)
525
+
526
+ if result["success"]:
527
+ stats = result.get("statistics", {})
528
+ return result["anonymized_text"], f"✅ موفق | شرکت: {stats.get('company',0)} | شخص: {stats.get('person',0)} | مبلغ: {stats.get('amount',0)} | درصد: {stats.get('percent',0)}"
529
+ return "", f"❌ {result.get('error', 'خطا')}"
530
+ except Exception as e:
531
+ return "", f"❌ {str(e)}"
532
+
533
+ def update_columns(file):
534
+ if file is None:
535
+ return gr.update(choices=[])
536
+ try:
537
+ df = pd.read_csv(file.name, encoding='utf-8', nrows=1)
538
+ except:
539
+ try:
540
+ df = pd.read_csv(file.name, encoding='utf-8-sig', nrows=1)
541
+ except:
542
+ df = pd.read_csv(file.name, encoding='cp1256', nrows=1)
543
+ return gr.update(choices=list(df.columns), value=df.columns[0])
544
+
545
+ def start_batch(file, text_col, delay, key):
546
+ if file is None:
547
+ yield 0, "❌ فایل انتخاب نشده", gr.update(visible=False)
548
+ return
549
+
550
+ api = key if key else os.getenv("CEREBRAS_API_KEY")
551
+ if not api:
552
+ yield 0, "❌ کلید API وارد نشده", gr.update(visible=False)
553
+ return
554
+
555
+ config = RateLimitConfig(min_delay_between_requests=float(delay))
556
+ processor = BatchProcessor(api_key=api, rate_limit_config=config)
557
+ batch_processor["instance"] = processor
558
+
559
+ for update in processor.process_csv(file.name, text_col):
560
+ if update["type"] == "error":
561
+ yield 0, f"❌ {update['message']}", gr.update(visible=False)
562
+ elif update["type"] == "progress":
563
+ yield update["progress"], f"📊 {update['current']}/{update['total']} | ✅ {update['processed']} | ❌ {update['failed']}", gr.update(visible=False)
564
+ elif update["type"] == "complete":
565
+ yield 100, f"✅ تکمیل! | کل: {update['total']} | موفق: {update['processed']} | ناموفق: {update['failed']} | زمان: {update['time']/60:.1f} دقیقه", gr.update(value=update['output_path'], visible=True)
566
+ elif update["type"] == "cancelled":
567
+ yield 0, f"⏹️ لغو شد | پردازش شده: {update['processed']}", gr.update(visible=False)
568
+
569
+ def cancel_batch():
570
+ if batch_processor["instance"]:
571
+ batch_processor["instance"].cancel()
572
+ return "⏹️ درخواست لغو..."
573
+
574
+ # اتصال رویدادها
575
+ process_btn.click(process_single, [input_text, api_key], [output_text, stats_output])
576
+ csv_file.change(update_columns, [csv_file], [text_column])
577
+ start_btn.click(start_batch, [csv_file, text_column, delay_slider, batch_api_key], [progress_bar, progress_text, output_file])
578
+ cancel_btn.click(cancel_batch, outputs=[progress_text])
579
+
580
+ return interface
581
+
582
+ # ============================================
583
+ # اجرا
584
+ # ============================================
585
+
586
+ if __name__ == "__main__":
587
+ interface = create_interface()
588
+ interface.launch(server_name="0.0.0.0", server_port=7860, share=True)