leilaghomashchi commited on
Commit
8fa3d6b
·
verified ·
1 Parent(s): dbe9d2f

Upload app_batch_anonymizer (1).py

Browse files
Files changed (1) hide show
  1. app_batch_anonymizer (1).py +946 -0
app_batch_anonymizer (1).py ADDED
@@ -0,0 +1,946 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import json
3
+ import gradio as gr
4
+ from typing import Dict, Any, List, Generator
5
+ import os
6
+ from dataclasses import dataclass
7
+ import re
8
+ import pandas as pd
9
+ import time
10
+ from datetime import datetime
11
+ import threading
12
+ from queue import Queue
13
+ import io
14
+
15
+ @dataclass
16
+ class CerebrasConfig:
17
+ """تنظیمات Cerebras API"""
18
+ api_key: str
19
+ base_url: str = "https://api.cerebras.ai/v1"
20
+ model: str = "llama-3.3-70b"
21
+ max_tokens: int = 2000
22
+ temperature: float = 0.1
23
+
24
+ @dataclass
25
+ class RateLimitConfig:
26
+ """تنظیمات محدودیت نرخ درخواست برای Cerebras"""
27
+ # محدودیت‌های Cerebras Free Tier
28
+ requests_per_minute: int = 30 # حداکثر درخواست در دقیقه
29
+ tokens_per_minute: int = 60000 # حداکثر توکن در دقیقه
30
+ min_delay_between_requests: float = 2.0 # حداقل تأخیر بین درخواست‌ها (ثانیه)
31
+ max_retries: int = 5 # حداکثر تلاش مجدد
32
+ initial_backoff: float = 5.0 # تأخیر اولیه برای backoff (ثانیه)
33
+ max_backoff: float = 120.0 # حداکثر تأخیر backoff (ثانیه)
34
+ backoff_multiplier: float = 2.0 # ضریب افزایش تأخیر
35
+
36
+ class RateLimiter:
37
+ """مدیریت محدودیت نرخ درخواست"""
38
+
39
+ def __init__(self, config: RateLimitConfig):
40
+ self.config = config
41
+ self.request_times: List[float] = []
42
+ self.lock = threading.Lock()
43
+ self.consecutive_failures = 0
44
+
45
+ def wait_if_needed(self) -> float:
46
+ """انتظار تا زمان مجاز ارسال درخواست بعدی"""
47
+ with self.lock:
48
+ now = time.time()
49
+
50
+ # پاک کردن درخواست‌های قدیمی‌تر از 1 دقیقه
51
+ self.request_times = [t for t in self.request_times if now - t < 60]
52
+
53
+ # محاسبه زمان انتظار
54
+ wait_time = 0.0
55
+
56
+ # اگر به محدودیت درخواست در دقیقه رسیده‌ایم
57
+ if len(self.request_times) >= self.config.requests_per_minute:
58
+ oldest_request = min(self.request_times)
59
+ wait_time = max(wait_time, 60 - (now - oldest_request) + 1)
60
+
61
+ # حداقل تأخیر بین درخواست‌ها
62
+ if self.request_times:
63
+ time_since_last = now - max(self.request_times)
64
+ if time_since_last < self.config.min_delay_between_requests:
65
+ wait_time = max(wait_time, self.config.min_delay_between_requests - time_since_last)
66
+
67
+ # افزایش تأخیر در صورت خطاهای متوالی
68
+ if self.consecutive_failures > 0:
69
+ failure_wait = min(
70
+ self.config.initial_backoff * (self.config.backoff_multiplier ** self.consecutive_failures),
71
+ self.config.max_backoff
72
+ )
73
+ wait_time = max(wait_time, failure_wait)
74
+
75
+ if wait_time > 0:
76
+ time.sleep(wait_time)
77
+
78
+ self.request_times.append(time.time())
79
+ return wait_time
80
+
81
+ def report_success(self):
82
+ """گزارش موفقیت درخواست"""
83
+ with self.lock:
84
+ self.consecutive_failures = 0
85
+
86
+ def report_failure(self, is_rate_limit: bool = False):
87
+ """گزارش شکست درخواست"""
88
+ with self.lock:
89
+ if is_rate_limit:
90
+ self.consecutive_failures += 1
91
+ else:
92
+ # برای خطاهای غیر rate limit، کمتر افزایش می‌دهیم
93
+ self.consecutive_failures = min(self.consecutive_failures + 0.5, 3)
94
+
95
+ def get_estimated_wait_time(self) -> float:
96
+ """تخمین زمان انتظار برای درخواست بعدی"""
97
+ with self.lock:
98
+ now = time.time()
99
+ self.request_times = [t for t in self.request_times if now - t < 60]
100
+
101
+ if len(self.request_times) >= self.config.requests_per_minute:
102
+ oldest_request = min(self.request_times)
103
+ return max(0, 60 - (now - oldest_request) + 1)
104
+
105
+ return self.config.min_delay_between_requests
106
+
107
+ class AdvancedCerebrasAnonymizer:
108
+ """سیستم پیشرفته ناشناس‌سازی متون مالی/خبری فارسی"""
109
+
110
+ def __init__(self, api_key: str = None, rate_limit_config: RateLimitConfig = None):
111
+ if api_key is None:
112
+ api_key = os.getenv("CEREBRAS_API_KEY")
113
+ if not api_key:
114
+ raise ValueError("کلید API یافت نشد")
115
+
116
+ self.config = CerebrasConfig(api_key=api_key)
117
+ self.rate_limit_config = rate_limit_config or RateLimitConfig()
118
+ self.rate_limiter = RateLimiter(self.rate_limit_config)
119
+ self.system_prompt = self._create_advanced_system_prompt()
120
+
121
+ def _create_advanced_system_prompt(self) -> str:
122
+ """ایجاد دستورالعمل سیستمی پیشرفته برای Cerebras"""
123
+ return """شما یک «ناشناس‌ساز متون مالی/خبری فارسی» هستید. وظیفه‌تان جایگزینی اسامی خاص و مقادیر عددی با شناسه‌های بی‌معناست.
124
+
125
+ ## **قوانین اندیس‌گذاری - CRITICAL**
126
+ ### **1. ترتیب شماره‌گذاری الزامی:**
127
+ - شرکت‌ها: company-01, company-02, company-03, company-04, ... (پیوسته و بدون گپ)
128
+ - اشخاص: person-01, person-02, person-03, ... (پیوسته و بدون گپ)
129
+ - اعداد: amount-01, amount-02, amount-03, ... (پیوسته و بدون گپ)
130
+ - درصدها: percent-01, percent-02, percent-03, ... (پیوسته و بدون گپ)
131
+
132
+ ### **2. ثبات شناسه‌ها در متن:**
133
+ - اگر "همراه اول" اول‌بار company-01 شد، در تمام متن همان باشد
134
+ - اگر "مهدی احمدی" اول‌بار person-01 شد، در تمام متن همان باشد
135
+
136
+ ### **3. تشخیص صحیح انواع:**
137
+ **شرکت/سازمان:** همراه اول، بانک ملی، ایران‌خودرو، سایپا، بانک مرکزی، سامانه کدال، وزارت نفت، سازمان تنظیم مقررات رادیویی، سازمان تامین اجتماعی
138
+ **⚠️ CRITICAL - گروه‌ها:** "گروه همراه اول"، "گروه اقتصادی آزادگان"، "گروه مالی صبا" → همه company-XX هستند (نه group-XX)
139
+ **⚠️ CRITICAL - کلمات عمومی:** "سه شرکت دارویی"، "چند بانک"، "یک شرکت" → کلمات عمومی هستند، موجودیت نیستند (حفظ شوند)
140
+ **⚠️ CRITICAL - نام‌های مستعار:** "فاما" همان "فولاد مبارکه اصفهان" است → هر دو company-01
141
+ **شخص:** مهدی اخوان بهابادی، محمدرضا فرزین، ابوالفضل نجارزاده
142
+ **عدد:** 37، 70، 677، 73.7، 178 (هر عددی)
143
+ **درصد:** 37 درصدی، 15 درصدی، 53 درصد، 43%
144
+
145
+ ## **مثال‌های صحیح:**
146
+
147
+ ### **مثال 1 (الگوی کامل):**
148
+ **ورودی:** مهدی اخوان بهابادی، مدیرعامل همراه اول، اعلام کرد درآمد عملیاتی شرکت با رشد 37 درصدی به 70 هزار و 677 میلیارد تومان رسیده است.
149
+ **خروجی صحیح:** person-01، مدیرعامل company-01، اعلام کرد درآمد عملیاتی شرکت با رشد percent-01 به amount-01 رسیده است.
150
+
151
+ ### **مثال 2:**
152
+ **ورودی:** بانک مرکزی و بانک ملی با همکاری محمدرضا فرزین، 60 درصد سپرده‌ها را مدیریت کردند.
153
+ **خروجی:** company-01 و company-02 با همکاری person-01، percent-01 سپرده‌ها را مدیریت کردند.
154
+
155
+ ## **⚠️ CRITICAL - دوره‌های زمانی را حفظ کن:**
156
+ - "۹ ماهه" → حفظ شود (نه amount-XX)
157
+ - "۵ ماهه سال" → حفظ شود (نه amount-XX)
158
+ - "۳ ماهه اول" → حفظ شود (نه amount-XX)
159
+
160
+ ## **موارد حفظ شده:**
161
+ - تاریخ‌ها: 1404/04/23، 30 آذر 1403، پاییز 1401
162
+ - فصل‌های سال: پاییز، بهار، تابستان، زمستان
163
+ - عناوین شغلی: مدیرعامل، رئیس کل، مدیرکل
164
+ - واحدها: میلیارد تومان، همت، ریال، ماه، سال
165
+ - مکان‌ها: تهران، اصفهان، ایران
166
+
167
+ **فقط متن ناشناس‌شده را برگردان - هیچ توضیح اضافی نیاز نیست.**
168
+ """
169
+
170
+ def _make_api_request_with_retry(self, text: str) -> Dict[str, Any]:
171
+ """ارسال درخواست به Cerebras API با مدیریت rate limit و retry"""
172
+ headers = {
173
+ "Authorization": f"Bearer {self.config.api_key}",
174
+ "Content-Type": "application/json"
175
+ }
176
+
177
+ payload = {
178
+ "messages": [
179
+ {"role": "system", "content": self.system_prompt},
180
+ {"role": "user", "content": text}
181
+ ],
182
+ "model": self.config.model,
183
+ "temperature": self.config.temperature,
184
+ "max_tokens": self.config.max_tokens
185
+ }
186
+
187
+ last_error = None
188
+
189
+ for attempt in range(self.rate_limit_config.max_retries):
190
+ # انتظار قبل از ارسال درخواست
191
+ wait_time = self.rate_limiter.wait_if_needed()
192
+
193
+ try:
194
+ response = requests.post(
195
+ f"{self.config.base_url}/chat/completions",
196
+ headers=headers,
197
+ json=payload,
198
+ timeout=60
199
+ )
200
+
201
+ # بررسی خطای rate limit (429)
202
+ if response.status_code == 429:
203
+ self.rate_limiter.report_failure(is_rate_limit=True)
204
+
205
+ # استخراج زمان انتظار از هدر (اگر موجود باشد)
206
+ retry_after = response.headers.get('Retry-After')
207
+ if retry_after:
208
+ wait_seconds = int(retry_after)
209
+ else:
210
+ # محاسبه exponential backoff
211
+ wait_seconds = min(
212
+ self.rate_limit_config.initial_backoff * (self.rate_limit_config.backoff_multiplier ** attempt),
213
+ self.rate_limit_config.max_backoff
214
+ )
215
+
216
+ last_error = f"محدودیت نرخ درخواست (429). تلاش {attempt + 1}/{self.rate_limit_config.max_retries}. انتظار {wait_seconds:.1f} ثانیه..."
217
+ time.sleep(wait_seconds)
218
+ continue
219
+
220
+ response.raise_for_status()
221
+ self.rate_limiter.report_success()
222
+ return response.json()
223
+
224
+ except requests.exceptions.Timeout:
225
+ self.rate_limiter.report_failure(is_rate_limit=False)
226
+ last_error = f"خطای timeout. تلاش {attempt + 1}/{self.rate_limit_config.max_retries}"
227
+ time.sleep(self.rate_limit_config.initial_backoff)
228
+
229
+ except requests.exceptions.RequestException as e:
230
+ self.rate_limiter.report_failure(is_rate_limit=False)
231
+ last_error = f"خطای شبکه: {str(e)}. تلاش {attempt + 1}/{self.rate_limit_config.max_retries}"
232
+ time.sleep(self.rate_limit_config.initial_backoff)
233
+
234
+ raise Exception(f"ناموفق پس از {self.rate_limit_config.max_retries} تلاش. آخرین خطا: {last_error}")
235
+
236
+ def anonymize_text(self, text: str) -> Dict[str, Any]:
237
+ """ناشناس‌سازی متن با استفاده از Cerebras"""
238
+ if not text or not text.strip():
239
+ return {
240
+ "success": False,
241
+ "error": "متن ورودی خالی است",
242
+ "anonymized_text": ""
243
+ }
244
+
245
+ try:
246
+ response = self._make_api_request_with_retry(text)
247
+
248
+ if "choices" not in response or not response["choices"]:
249
+ return {
250
+ "success": False,
251
+ "error": "پاسخ نامعتبر از API",
252
+ "anonymized_text": ""
253
+ }
254
+
255
+ content = response["choices"][0]["message"]["content"]
256
+ content = self._clean_markdown(content)
257
+ content = content.strip()
258
+
259
+ analysis = self._analyze_anonymized_text(content)
260
+
261
+ return {
262
+ "success": True,
263
+ "anonymized_text": content,
264
+ "entities": analysis["entities"],
265
+ "statistics": analysis["statistics"],
266
+ "usage": response.get("usage", {})
267
+ }
268
+
269
+ except Exception as e:
270
+ return {
271
+ "success": False,
272
+ "error": f"خطا در پردازش: {str(e)}",
273
+ "anonymized_text": ""
274
+ }
275
+
276
+ def _clean_markdown(self, content: str) -> str:
277
+ """پاک کردن markdown از پاسخ"""
278
+ if "```" in content:
279
+ lines = content.split('\n')
280
+ clean_lines = []
281
+ skip = False
282
+ for line in lines:
283
+ if line.strip().startswith('```'):
284
+ skip = not skip
285
+ continue
286
+ if not skip:
287
+ clean_lines.append(line)
288
+ content = '\n'.join(clean_lines)
289
+ return content
290
+
291
+ def _analyze_anonymized_text(self, text: str) -> Dict[str, Any]:
292
+ """تحلیل متن ناشناس‌سازی شده"""
293
+ companies = re.findall(r'company-(\d+)', text)
294
+ persons = re.findall(r'person-(\d+)', text)
295
+ amounts = re.findall(r'amount-(\d+)', text)
296
+ percents = re.findall(r'percent-(\d+)', text)
297
+
298
+ statistics = {
299
+ "company": len(set(companies)),
300
+ "person": len(set(persons)),
301
+ "amount": len(set(amounts)),
302
+ "percent": len(set(percents)),
303
+ "total_replacements": len(companies) + len(persons) + len(amounts) + len(percents)
304
+ }
305
+
306
+ entities = {
307
+ "companies": sorted(list(set(companies)), key=lambda x: int(x)),
308
+ "persons": sorted(list(set(persons)), key=lambda x: int(x)),
309
+ "amounts": sorted(list(set(amounts)), key=lambda x: int(x)),
310
+ "percents": sorted(list(set(percents)), key=lambda x: int(x))
311
+ }
312
+
313
+ return {
314
+ "statistics": statistics,
315
+ "entities": entities
316
+ }
317
+
318
+
319
+ class BatchProcessor:
320
+ """پردازشگر د��ته‌ای فایل‌های CSV"""
321
+
322
+ def __init__(self, api_key: str, rate_limit_config: RateLimitConfig = None):
323
+ self.api_key = api_key
324
+ self.rate_limit_config = rate_limit_config or RateLimitConfig()
325
+ self.anonymizer = None
326
+ self.is_cancelled = False
327
+ self.current_progress = 0
328
+ self.total_rows = 0
329
+ self.processed_rows = 0
330
+ self.failed_rows = 0
331
+ self.start_time = None
332
+
333
+ def cancel(self):
334
+ """لغو پردازش"""
335
+ self.is_cancelled = True
336
+
337
+ def reset(self):
338
+ """بازنشانی وضعیت"""
339
+ self.is_cancelled = False
340
+ self.current_progress = 0
341
+ self.total_rows = 0
342
+ self.processed_rows = 0
343
+ self.failed_rows = 0
344
+ self.start_time = None
345
+
346
+ def process_csv(
347
+ self,
348
+ file_path: str,
349
+ text_column: str,
350
+ output_column: str = "anonymized_text",
351
+ progress_callback=None
352
+ ) -> Generator[Dict[str, Any], None, None]:
353
+ """پردازش فایل CSV به صورت streaming"""
354
+
355
+ self.reset()
356
+ self.start_time = time.time()
357
+
358
+ # خواندن فایل CSV
359
+ try:
360
+ df = pd.read_csv(file_path, encoding='utf-8')
361
+ except UnicodeDecodeError:
362
+ try:
363
+ df = pd.read_csv(file_path, encoding='utf-8-sig')
364
+ except:
365
+ df = pd.read_csv(file_path, encoding='cp1256')
366
+
367
+ if text_column not in df.columns:
368
+ yield {
369
+ "type": "error",
370
+ "message": f"ستون '{text_column}' در فایل یافت نشد. ستون‌های موجود: {list(df.columns)}"
371
+ }
372
+ return
373
+
374
+ self.total_rows = len(df)
375
+
376
+ # ایجاد anonymizer
377
+ self.anonymizer = AdvancedCerebrasAnonymizer(
378
+ api_key=self.api_key,
379
+ rate_limit_config=self.rate_limit_config
380
+ )
381
+
382
+ # ایجاد ستون خروجی
383
+ df[output_column] = ""
384
+ df["anonymization_status"] = ""
385
+ df["entities_found"] = ""
386
+
387
+ yield {
388
+ "type": "info",
389
+ "message": f"🚀 شروع پردازش {self.total_rows} ردیف...",
390
+ "total": self.total_rows
391
+ }
392
+
393
+ results = []
394
+
395
+ for idx, row in df.iterrows():
396
+ if self.is_cancelled:
397
+ yield {
398
+ "type": "cancelled",
399
+ "message": "پردازش توسط کاربر لغو شد",
400
+ "processed": self.processed_rows,
401
+ "failed": self.failed_rows
402
+ }
403
+ break
404
+
405
+ text = str(row[text_column]) if pd.notna(row[text_column]) else ""
406
+
407
+ if not text.strip():
408
+ df.at[idx, output_column] = ""
409
+ df.at[idx, "anonymization_status"] = "خالی"
410
+ df.at[idx, "entities_found"] = ""
411
+ self.processed_rows += 1
412
+ continue
413
+
414
+ # پردازش متن
415
+ result = self.anonymizer.anonymize_text(text)
416
+
417
+ if result["success"]:
418
+ df.at[idx, output_column] = result["anonymized_text"]
419
+ df.at[idx, "anonymization_status"] = "موفق"
420
+ stats = result.get("statistics", {})
421
+ entities_summary = f"شرکت:{stats.get('company',0)} | شخص:{stats.get('person',0)} | مبلغ:{stats.get('amount',0)} | درصد:{stats.get('percent',0)}"
422
+ df.at[idx, "entities_found"] = entities_summary
423
+ self.processed_rows += 1
424
+ else:
425
+ df.at[idx, output_column] = f"خطا: {result.get('error', 'نامشخص')}"
426
+ df.at[idx, "anonymization_status"] = "ناموفق"
427
+ df.at[idx, "entities_found"] = ""
428
+ self.failed_rows += 1
429
+
430
+ # محاسبه پیشرفت و زمان باقیمانده
431
+ self.current_progress = (idx + 1) / self.total_rows * 100
432
+ elapsed = time.time() - self.start_time
433
+ avg_time_per_row = elapsed / (idx + 1)
434
+ remaining_rows = self.total_rows - (idx + 1)
435
+ estimated_remaining = avg_time_per_row * remaining_rows
436
+
437
+ # تخمین زمان انتظار بعدی
438
+ next_wait = self.anonymizer.rate_limiter.get_estimated_wait_time()
439
+
440
+ yield {
441
+ "type": "progress",
442
+ "current": idx + 1,
443
+ "total": self.total_rows,
444
+ "progress": self.current_progress,
445
+ "processed": self.processed_rows,
446
+ "failed": self.failed_rows,
447
+ "elapsed": elapsed,
448
+ "estimated_remaining": estimated_remaining,
449
+ "next_wait": next_wait,
450
+ "last_result": result
451
+ }
452
+
453
+ # ذخیره نتیجه نهایی
454
+ if not self.is_cancelled:
455
+ output_path = file_path.replace('.csv', '_anonymized.csv')
456
+ if output_path == file_path:
457
+ output_path = file_path + '_anonymized.csv'
458
+
459
+ df.to_csv(output_path, index=False, encoding='utf-8-sig')
460
+
461
+ total_time = time.time() - self.start_time
462
+
463
+ yield {
464
+ "type": "complete",
465
+ "message": "✅ پردازش با موفقیت تکمیل شد!",
466
+ "output_path": output_path,
467
+ "total": self.total_rows,
468
+ "processed": self.processed_rows,
469
+ "failed": self.failed_rows,
470
+ "total_time": total_time,
471
+ "dataframe": df
472
+ }
473
+
474
+
475
+ def create_batch_interface():
476
+ """ایجاد رابط کاربری برای پردازش دسته‌ای"""
477
+
478
+ api_key_available = bool(os.getenv("CEREBRAS_API_KEY"))
479
+
480
+ custom_css = """
481
+ .gradio-container {
482
+ font-family: 'Tahoma', 'Arial', sans-serif !important;
483
+ direction: rtl;
484
+ max-width: 1400px;
485
+ margin: 0 auto;
486
+ }
487
+ .progress-bar {
488
+ background-color: #e9ecef;
489
+ border-radius: 10px;
490
+ height: 30px;
491
+ overflow: hidden;
492
+ }
493
+ .progress-fill {
494
+ background: linear-gradient(90deg, #28a745, #20c997);
495
+ height: 100%;
496
+ transition: width 0.3s ease;
497
+ }
498
+ .stats-card {
499
+ background-color: #f8f9fa;
500
+ border-radius: 10px;
501
+ padding: 15px;
502
+ margin: 10px 0;
503
+ border: 1px solid #dee2e6;
504
+ }
505
+ .warning-box {
506
+ background-color: #fff3cd;
507
+ border: 2px solid #ffeaa7;
508
+ border-radius: 12px;
509
+ padding: 15px;
510
+ color: #856404;
511
+ margin: 10px 0;
512
+ }
513
+ .success-box {
514
+ background-color: #d4edda;
515
+ border: 2px solid #c3e6cb;
516
+ border-radius: 12px;
517
+ padding: 15px;
518
+ color: #155724;
519
+ margin: 10px 0;
520
+ }
521
+ .info-box {
522
+ background-color: #d1ecf1;
523
+ border: 2px solid #bee5eb;
524
+ border-radius: 12px;
525
+ padding: 15px;
526
+ color: #0c5460;
527
+ margin: 10px 0;
528
+ }
529
+ """
530
+
531
+ # متغیرهای سراسری برای مدیریت پردازش
532
+ batch_processor = {"instance": None}
533
+
534
+ with gr.Blocks(css=custom_css, title="پردازش دسته‌ای ناشناس‌سازی با Cerebras", theme=gr.themes.Soft()) as interface:
535
+
536
+ gr.Markdown("""
537
+ # 🔒 سیستم پردازش دسته‌ای ناشناس‌سازی متون فارسی
538
+ ### ⚡ قدرت‌گرفته از Cerebras AI با مدیریت هوشمند Rate Limit
539
+ """)
540
+
541
+ with gr.Tabs():
542
+ # تب پردازش تکی
543
+ with gr.Tab("📝 پردازش تکی"):
544
+ if api_key_available:
545
+ gr.Markdown('<div class="success-box">✅ <strong>سیستم آماده است</strong> - کلید API تنظیم شده</div>')
546
+ single_api_key = gr.Textbox(visible=False, value="")
547
+ else:
548
+ gr.Markdown('<div class="warning-box">⚠️ <strong>کلید API تنظیم نشده</strong> - لطفاً کلید خود را وارد کنید</div>')
549
+ single_api_key = gr.Textbox(label="🔑 کلید Cerebras API", placeholder="csk-...", type="password")
550
+
551
+ with gr.Row():
552
+ with gr.Column():
553
+ single_input = gr.Textbox(label="📝 متن ورودی", placeholder="متن خود را وارد کنید...", lines=10)
554
+ single_btn = gr.Button("🔒 ناشناس‌سازی", variant="primary")
555
+ with gr.Column():
556
+ single_output = gr.Textbox(label="🎯 متن ناشناس‌سازی شده", lines=10)
557
+ single_stats = gr.Markdown()
558
+
559
+ # تب پردازش دسته‌ای
560
+ with gr.Tab("📁 پردازش دسته‌ای CSV"):
561
+ gr.Markdown("""
562
+ <div class="info-box">
563
+ 📌 <strong>راهنمای پردازش دسته‌ای:</strong><br>
564
+ 1. فایل CSV خود را آپلود کنید<br>
565
+ 2. ستون حاوی متن را انتخاب کنید<br>
566
+ 3. تنظیمات Rate Limit را بررسی کنید<br>
567
+ 4. پردازش را شروع کنید<br><br>
568
+ ⚠️ <strong>نکته مهم:</strong> برای جلوگیری از خطای 429، تأخیر بین درخواست‌ها به صورت خودکار مدیریت می‌شود.
569
+ </div>
570
+ """)
571
+
572
+ if not api_key_available:
573
+ batch_api_key = gr.Textbox(label="🔑 کلید Cerebras API", placeholder="csk-...", type="password")
574
+ else:
575
+ batch_api_key = gr.Textbox(visible=False, value="")
576
+
577
+ with gr.Row():
578
+ with gr.Column(scale=2):
579
+ csv_file = gr.File(label="📂 فایل CSV", file_types=[".csv"])
580
+
581
+ with gr.Row():
582
+ text_column = gr.Dropdown(
583
+ label="📑 ستون متن",
584
+ choices=[],
585
+ interactive=True,
586
+ info="ستون حاوی متن برای ناشناس‌سازی"
587
+ )
588
+ output_column = gr.Textbox(
589
+ label="📤 نام ستون خروجی",
590
+ value="anonymized_text",
591
+ info="نام ستون برای ذخیره نتایج"
592
+ )
593
+
594
+ with gr.Column(scale=1):
595
+ gr.Markdown("### ⚙️ تنظیمات Rate Limit")
596
+
597
+ delay_between_requests = gr.Slider(
598
+ label="⏱️ حداقل تأخیر بین درخواست‌ها (ثانیه)",
599
+ minimum=1.0,
600
+ maximum=10.0,
601
+ value=2.5,
602
+ step=0.5,
603
+ info="افزایش این مقدار از خطای 429 جلوگیری می‌کند"
604
+ )
605
+
606
+ requests_per_minute = gr.Slider(
607
+ label="📊 حداکثر درخواست در دقیقه",
608
+ minimum=5,
609
+ maximum=30,
610
+ value=20,
611
+ step=1,
612
+ info="محدودیت Cerebras Free: 30 درخواست/دقیقه"
613
+ )
614
+
615
+ max_retries = gr.Slider(
616
+ label="🔄 حداکثر تلاش مجدد",
617
+ minimum=1,
618
+ maximum=10,
619
+ value=5,
620
+ step=1,
621
+ info="تعداد تلاش در صورت خطای 429"
622
+ )
623
+
624
+ with gr.Row():
625
+ start_btn = gr.Button("🚀 شروع پردازش", variant="primary", size="lg")
626
+ cancel_btn = gr.Button("⏹️ لغو پردازش", variant="stop", size="lg")
627
+
628
+ # نمایش پیشرفت
629
+ progress_bar = gr.Slider(
630
+ label="📊 پیشرفت کلی",
631
+ minimum=0,
632
+ maximum=100,
633
+ value=0,
634
+ interactive=False
635
+ )
636
+
637
+ with gr.Row():
638
+ with gr.Column():
639
+ progress_text = gr.Markdown("### 📈 وضعیت پردازش\nدر انتظار شروع...")
640
+ with gr.Column():
641
+ time_stats = gr.Markdown("### ⏱️ زمان‌بندی\nدر انتظار شروع...")
642
+
643
+ # لاگ پردازش
644
+ process_log = gr.Textbox(
645
+ label="📋 لاگ پردازش",
646
+ lines=8,
647
+ max_lines=15,
648
+ interactive=False
649
+ )
650
+
651
+ # نمایش نمونه نتایج
652
+ with gr.Accordion("👁️ پیش‌نمایش نتایج", open=False):
653
+ preview_table = gr.Dataframe(
654
+ label="نمونه نتایج",
655
+ headers=["متن اصلی", "متن ناشناس‌شده", "وضعیت"],
656
+ interactive=False
657
+ )
658
+
659
+ # دانلود نتیجه
660
+ output_file = gr.File(label="📥 دانلود فایل خروجی", visible=False)
661
+
662
+ # تب تنظیمات
663
+ with gr.Tab("⚙️ تنظیمات و راهنما"):
664
+ gr.Markdown("""
665
+ ## 📖 راهنمای محدودیت‌های Cerebras API
666
+
667
+ ### 🔒 محدودیت‌های Free Tier:
668
+ | پارامتر | مقدار |
669
+ |---------|--------|
670
+ | درخواست در دقیقه | 30 |
671
+ | توکن در دقیقه | 60,000 |
672
+ | توکن در روز | 1,000,000 |
673
+
674
+ ### ⚡ نکات بهینه‌سازی:
675
+ 1. **تأخیر بین درخواست‌ها:** حداقل 2 ثانیه بین هر درخواست
676
+ 2. **Exponential Backoff:** در صورت خطای 429، تأخیر ��ه صورت نمایی افزایش می‌یابد
677
+ 3. **Retry Logic:** سیستم تا 5 بار تلاش مجدد می‌کند
678
+
679
+ ### 🎯 پیشنهادات:
680
+ - برای فایل‌های بزرگ (>100 ردیف)، تأخیر را روی 3 ثانیه تنظیم کنید
681
+ - اگر خطای 429 زیاد دیدید، تأخیر را افزایش دهید
682
+ - در ساعات شلوغ، محدودیت‌ها ممکن است سخت‌تر شوند
683
+
684
+ ### 📊 فرمت فایل CSV:
685
+ - **Encoding:** UTF-8 یا UTF-8-BOM پیشنهاد می‌شود
686
+ - **ستون‌ها:** حداقل یک ستون حاوی متن فارسی
687
+ - **حجم:** بدون محدودیت (اما پردازش فایل‌های بزرگ زمان‌بر است)
688
+ """)
689
+
690
+ # توابع
691
+ def update_columns(file):
692
+ """بروزرسانی لیست ستون‌ها بعد از آپلود فایل"""
693
+ if file is None:
694
+ return gr.update(choices=[], value=None)
695
+
696
+ try:
697
+ df = pd.read_csv(file.name, encoding='utf-8', nrows=5)
698
+ except:
699
+ try:
700
+ df = pd.read_csv(file.name, encoding='utf-8-sig', nrows=5)
701
+ except:
702
+ df = pd.read_csv(file.name, encoding='cp1256', nrows=5)
703
+
704
+ columns = list(df.columns)
705
+ return gr.update(choices=columns, value=columns[0] if columns else None)
706
+
707
+ def process_single_text(text, api_key):
708
+ """پردازش متن تکی"""
709
+ if not text.strip():
710
+ return "", "⚠️ متن ورودی خالی است"
711
+
712
+ key = api_key if api_key else os.getenv("CEREBRAS_API_KEY")
713
+ if not key:
714
+ return "", "❌ کلید API وارد نشده است"
715
+
716
+ try:
717
+ anonymizer = AdvancedCerebrasAnonymizer(api_key=key)
718
+ result = anonymizer.anonymize_text(text)
719
+
720
+ if result["success"]:
721
+ stats = result.get("statistics", {})
722
+ stats_md = f"""
723
+ ### ✅ پردازش موفق
724
+ - 🏢 شرکت‌ها: {stats.get('company', 0)}
725
+ - 👤 اشخاص: {stats.get('person', 0)}
726
+ - 💰 مبالغ: {stats.get('amount', 0)}
727
+ - 📊 درصدها: {stats.get('percent', 0)}
728
+ """
729
+ return result["anonymized_text"], stats_md
730
+ else:
731
+ return "", f"❌ خطا: {result.get('error', 'نامشخص')}"
732
+ except Exception as e:
733
+ return "", f"❌ خطا: {str(e)}"
734
+
735
+ def start_batch_processing(
736
+ file,
737
+ text_col,
738
+ output_col,
739
+ delay,
740
+ rpm,
741
+ retries,
742
+ api_key
743
+ ):
744
+ """شروع پردازش دسته‌ای"""
745
+ if file is None:
746
+ yield (
747
+ 0,
748
+ "### ❌ خطا\nفایل انتخاب نشده است",
749
+ "",
750
+ "",
751
+ None,
752
+ gr.update(visible=False)
753
+ )
754
+ return
755
+
756
+ key = api_key if api_key else os.getenv("CEREBRAS_API_KEY")
757
+ if not key:
758
+ yield (
759
+ 0,
760
+ "### ❌ خطا\nکلید API وارد نشده است",
761
+ "",
762
+ "",
763
+ None,
764
+ gr.update(visible=False)
765
+ )
766
+ return
767
+
768
+ # تنظیم rate limit
769
+ rate_config = RateLimitConfig(
770
+ requests_per_minute=int(rpm),
771
+ min_delay_between_requests=float(delay),
772
+ max_retries=int(retries)
773
+ )
774
+
775
+ # ایجاد پردازشگر
776
+ processor = BatchProcessor(api_key=key, rate_limit_config=rate_config)
777
+ batch_processor["instance"] = processor
778
+
779
+ log_lines = []
780
+ preview_data = []
781
+
782
+ # پردازش
783
+ for update in processor.process_csv(file.name, text_col, output_col):
784
+ update_type = update.get("type")
785
+
786
+ if update_type == "error":
787
+ log_lines.append(f"❌ {update['message']}")
788
+ yield (
789
+ 0,
790
+ f"### ❌ خطا\n{update['message']}",
791
+ "",
792
+ "\n".join(log_lines),
793
+ None,
794
+ gr.update(visible=False)
795
+ )
796
+ return
797
+
798
+ elif update_type == "info":
799
+ log_lines.append(f"ℹ️ {update['message']}")
800
+
801
+ elif update_type == "progress":
802
+ progress = update["progress"]
803
+ current = update["current"]
804
+ total = update["total"]
805
+ processed = update["processed"]
806
+ failed = update["failed"]
807
+ elapsed = update["elapsed"]
808
+ remaining = update["estimated_remaining"]
809
+ next_wait = update.get("next_wait", 0)
810
+
811
+ progress_md = f"""
812
+ ### 📈 وضعیت پردازش
813
+ - **پردازش شده:** {current}/{total} ({progress:.1f}%)
814
+ - **موفق:** {processed} ✅
815
+ - **ناموفق:** {failed} ❌
816
+ - **تأخیر بعدی:** {next_wait:.1f} ثانیه
817
+ """
818
+
819
+ time_md = f"""
820
+ ### ⏱️ زمان‌بندی
821
+ - **سپری شده:** {elapsed/60:.1f} دقیقه
822
+ - **تخمین باقیمانده:** {remaining/60:.1f} دقیقه
823
+ - **سرعت:** {current/elapsed*60:.1f} ردیف/دقیقه
824
+ """
825
+
826
+ # بروزرسانی لاگ هر 10 ردیف
827
+ if current % 10 == 0 or current == total:
828
+ log_lines.append(f"📊 پردازش {current}/{total} - موفق: {processed}, ناموفق: {failed}")
829
+
830
+ # بروزرسانی پیش‌نمایش
831
+ last_result = update.get("last_result", {})
832
+ if last_result.get("success"):
833
+ preview_data.append([
834
+ "...", # متن اصلی خلاصه
835
+ last_result.get("anonymized_text", "")[:100] + "...",
836
+ "✅ موفق"
837
+ ])
838
+ if len(preview_data) > 5:
839
+ preview_data = preview_data[-5:]
840
+
841
+ yield (
842
+ progress,
843
+ progress_md,
844
+ time_md,
845
+ "\n".join(log_lines[-20:]), # فقط 20 خط آخر
846
+ preview_data if preview_data else None,
847
+ gr.update(visible=False)
848
+ )
849
+
850
+ elif update_type == "cancelled":
851
+ log_lines.append(f"⏹️ {update['message']}")
852
+ yield (
853
+ 0,
854
+ f"### ⏹️ لغو شد\nپردازش شده: {update['processed']}, ناموفق: {update['failed']}",
855
+ "",
856
+ "\n".join(log_lines),
857
+ preview_data if preview_data else None,
858
+ gr.update(visible=False)
859
+ )
860
+ return
861
+
862
+ elif update_type == "complete":
863
+ total_time = update["total_time"]
864
+ log_lines.append(f"✅ {update['message']}")
865
+ log_lines.append(f"📁 فایل خروجی: {update['output_path']}")
866
+
867
+ progress_md = f"""
868
+ ### ✅ پردازش تکمیل شد!
869
+ - **کل ردیف‌ها:** {update['total']}
870
+ - **موفق:** {update['processed']} ✅
871
+ - **ناموفق:** {update['failed']} ❌
872
+ - **زمان کل:** {total_time/60:.1f} دقیقه
873
+ """
874
+
875
+ time_md = f"""
876
+ ### 📊 آمار نهایی
877
+ - **سرعت میانگین:** {update['total']/total_time*60:.1f} ردیف/دقیقه
878
+ - **نرخ موفقیت:** {update['processed']/update['total']*100:.1f}%
879
+ """
880
+
881
+ yield (
882
+ 100,
883
+ progress_md,
884
+ time_md,
885
+ "\n".join(log_lines),
886
+ preview_data if preview_data else None,
887
+ gr.update(value=update['output_path'], visible=True)
888
+ )
889
+
890
+ def cancel_processing():
891
+ """لغو پردازش"""
892
+ if batch_processor["instance"]:
893
+ batch_processor["instance"].cancel()
894
+ return "⏹️ درخواست لغو ارسال شد..."
895
+
896
+ # اتصال رویدادها
897
+ csv_file.change(
898
+ fn=update_columns,
899
+ inputs=[csv_file],
900
+ outputs=[text_column]
901
+ )
902
+
903
+ single_btn.click(
904
+ fn=process_single_text,
905
+ inputs=[single_input, single_api_key],
906
+ outputs=[single_output, single_stats]
907
+ )
908
+
909
+ start_btn.click(
910
+ fn=start_batch_processing,
911
+ inputs=[
912
+ csv_file,
913
+ text_column,
914
+ output_column,
915
+ delay_between_requests,
916
+ requests_per_minute,
917
+ max_retries,
918
+ batch_api_key
919
+ ],
920
+ outputs=[
921
+ progress_bar,
922
+ progress_text,
923
+ time_stats,
924
+ process_log,
925
+ preview_table,
926
+ output_file
927
+ ]
928
+ )
929
+
930
+ cancel_btn.click(
931
+ fn=cancel_processing,
932
+ outputs=[process_log]
933
+ )
934
+
935
+ return interface
936
+
937
+
938
+ # اجرای برنامه
939
+ if __name__ == "__main__":
940
+ interface = create_batch_interface()
941
+ interface.launch(
942
+ server_name="0.0.0.0",
943
+ server_port=7860,
944
+ share=True,
945
+ show_error=True
946
+ )