leilaghomashchi commited on
Commit
88b9caa
·
verified ·
1 Parent(s): 965712b

Upload app_batch_rate_limit_fixed (4).py

Browse files
Files changed (1) hide show
  1. app_batch_rate_limit_fixed (4).py +834 -0
app_batch_rate_limit_fixed (4).py ADDED
@@ -0,0 +1,834 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import json
3
+ import gradio as gr
4
+ from typing import Dict, Any, List, Optional
5
+ import os
6
+ from dataclasses import dataclass
7
+ import re
8
+ import pandas as pd
9
+ import time
10
+ import random
11
+ from io import StringIO
12
+ from pathlib import Path
13
+
14
+ @dataclass
15
+ class CerebrasConfig:
16
+ """تنظیمات Cerebras API"""
17
+ api_key: str
18
+ base_url: str = "https://api.cerebras.ai/v1"
19
+ model: str = "llama-3.3-70b"
20
+ max_tokens: int = 2000
21
+ temperature: float = 0.1
22
+
23
+ # ============= تنظیمات Rate Limit =============
24
+ # ⚠️ Cerebras Free Tier: حدود 30 request/minute
25
+ DELAY_BETWEEN_REQUESTS = 8.0 # ⭐ ثانیه تاخیر بین هر درخواست (8 ثانیه = ~7.5 req/min)
26
+ MAX_RETRIES = 5 # کاهش تعداد retry
27
+ INITIAL_BACKOFF = 30.0 # ⭐ تاخیر اولیه بیشتر
28
+ BACKOFF_MULTIPLIER = 1.5 # ⭐ ضریب کمتر
29
+ MAX_BACKOFF = 120.0 # حداکثر تاخیر (2 دقیقه)
30
+ CHECKPOINT_INTERVAL = 5 # هر چند ردیف یکبار ذخیره شود
31
+
32
+ class AdvancedCerebrasAnonymizer:
33
+ """سیستم پیشرفته ناشناس‌سازی متون مالی/خبری فارسی"""
34
+
35
+ def __init__(self, api_key: str = None):
36
+ if api_key is None:
37
+ api_key = os.getenv("CEREBRAS_API_KEY")
38
+ if not api_key:
39
+ raise ValueError("کلید API یافت نشد")
40
+
41
+ self.config = CerebrasConfig(api_key=api_key)
42
+ self.system_prompt = self._create_advanced_system_prompt()
43
+ self.last_request_time = 0
44
+ self.request_count = 0
45
+
46
+ def _create_advanced_system_prompt(self) -> str:
47
+ """ایجاد دستورالعمل سیستمی پیشرفته برای Cerebras"""
48
+ return """شما یک «ناشناس‌ساز متون مالی/خبری فارسی» هستید. وظیفه‌تان جایگزینی اسامی خاص و مقادیر عددی با شناسه‌های بی‌معناست.
49
+
50
+ ## **قوانین اندیس‌گذاری - CRITICAL**
51
+ ### **1. ترتیب شماره‌گذاری الزامی:**
52
+ - شرکت‌ها: company-01, company-02, company-03, company-04, ... (پیوسته و بدون گپ)
53
+ - اشخاص: person-01, person-02, person-03, ... (پیوسته و بدون گپ)
54
+ - اعداد: amount-01, amount-02, amount-03, ... (پیوسته و بدون گپ)
55
+ - درصدها: percent-01, percent-02, percent-03, ... (پیوسته و بدون گپ)
56
+
57
+ ### **2. ثبات شناسه‌ها در متن:**
58
+ - اگر "همراه اول" اول‌بار company-01 شد، در تمام متن همان باشد
59
+ - اگر "مهدی احمدی" اول‌بار person-01 شد، در تمام متن همان باشد
60
+
61
+ ### **3. تشخیص صحیح انواع:**
62
+ **شرکت/سازمان:** همراه اول، بانک ملی، ایران‌خودرو، سایپا، بانک مرکزی، سامانه کدال، وزارت نفت، سازمان تنظیم مقررات رادیویی، سازمان تامین اجتماعی
63
+ **⚠️ CRITICAL - گروه‌ها:** "گروه همراه اول"، "گروه اقتصادی آزادگان"، "گروه مالی صبا" → همه company-XX هستند (نه group-XX)
64
+ **⚠️ CRITICAL - کلمات عمومی:** "سه شرکت دارویی"، "چند بانک"، "یک شرکت" → کلمات عمومی هستند، موجودیت نیستند (حفظ شوند)
65
+ **⚠️ CRITICAL - نام‌های مستعار:** "فاما" همان "فولاد مبارکه اصفهان" است → هر دو company-01
66
+ **شخص:** مهدی اخوان بهابادی، محمدرضا فرزین، ابوالفضل نجارزاده
67
+ **عدد:** 37، 70، 677، 73.7، 178 (هر عددی)
68
+ **درصد:** 37 درصدی، 15 درصدی، 53 درصد، 43%
69
+
70
+ ## **مثال‌های صحیح:**
71
+
72
+ ### **مثال 1 (الگوی کامل):**
73
+ **ورودی:** مهدی اخوان بهابادی، مدیرعامل همراه اول، اعلام کرد درآمد عملیاتی شرکت با رشد 37 درصدی به 70 هزار و 677 میلیارد تومان رسیده است. سود خالص 7101 میلیارد تومان و تلفیقی گروه همراه اول 8003 میلیارد تومان شد.
74
+ **خروجی صحیح:** person-01، مدیرعامل company-01، اعلام کرد درآمد عملیاتی شرکت با رشد percent-01 به amount-01 رسیده است. سود خالص amount-02 و تلفیقی گروه company-01 amount-03 شد.
75
+
76
+ ### **مثال 2:**
77
+ **ورودی:** بانک مرکزی و بانک ملی با همکاری محمدرضا فرزین، 60 درصد سپرده‌ها را مدیریت کردند.
78
+ **خروجی:** company-01 و company-02 با همکاری person-01، percent-01 سپرده‌ها را مدیریت کردند.
79
+
80
+ ## **موارد حفظ شده:**
81
+ - تاریخ‌ها: 1404/04/23، 30 آذر 1403، پاییز 1401
82
+ - فصل‌های سال: پاییز، بهار، تابستان، زمستان
83
+ - عناوین شغلی: مدیرعامل، رئیس کل، مدیرکل
84
+ - واحدها: میلیارد تومان، همت، ریال، ماه، سال
85
+ - مکان‌ها: تهران، اصفهان، ایران
86
+ - کلمات عمومی: "سه شرکت دارویی"، "چند بانک"، "یک شرکت"، "مراکز درمانی"
87
+ - دوره‌های زمانی: "۵ ماهه سال"، "۹ ماهه"، "۳ ماهه اول"
88
+
89
+ ## **ممنوع:**
90
+ - کلمات انگلیسی اضافی
91
+ - تغییر ساختار جمله
92
+ - حذف یا اضافه کردن کلمات
93
+ - استفاده از group-XX - همه گروه‌ها باید company-XX باشند
94
+
95
+ **فقط متن ناشناس‌شده را برگردان - هیچ توضیح اضافی نیاز نیست.**
96
+ """
97
+
98
+ def _wait_for_rate_limit(self):
99
+ """اطمینان از رعایت فاصله بین درخواست‌ها"""
100
+ elapsed = time.time() - self.last_request_time
101
+ if elapsed < DELAY_BETWEEN_REQUESTS:
102
+ sleep_time = DELAY_BETWEEN_REQUESTS - elapsed
103
+ print(f" ⏳ انتظار {sleep_time:.1f} ثانیه برای رعایت rate limit...")
104
+ time.sleep(sleep_time)
105
+
106
+ def _make_api_request(self, text: str) -> Dict[str, Any]:
107
+ """ارسال درخواست به Cerebras API با retry logic پیشرفته"""
108
+ headers = {
109
+ "Authorization": f"Bearer {self.config.api_key}",
110
+ "Content-Type": "application/json"
111
+ }
112
+
113
+ payload = {
114
+ "messages": [
115
+ {"role": "system", "content": self.system_prompt},
116
+ {"role": "user", "content": text}
117
+ ],
118
+ "model": self.config.model,
119
+ "temperature": self.config.temperature,
120
+ "max_tokens": self.config.max_tokens
121
+ }
122
+
123
+ for attempt in range(MAX_RETRIES):
124
+ try:
125
+ # ⭐ رعایت فاصله بین درخواست‌ها
126
+ self._wait_for_rate_limit()
127
+
128
+ response = requests.post(
129
+ f"{self.config.base_url}/chat/completions",
130
+ headers=headers,
131
+ json=payload,
132
+ timeout=90 # افزایش timeout
133
+ )
134
+
135
+ # ⭐ بررسی خطای 429 قبل از raise_for_status
136
+ if response.status_code == 429:
137
+ raise requests.exceptions.HTTPError("429 Too Many Requests")
138
+
139
+ response.raise_for_status()
140
+
141
+ self.last_request_time = time.time()
142
+ self.request_count += 1
143
+
144
+ return response.json()
145
+
146
+ except requests.exceptions.RequestException as e:
147
+ error_str = str(e)
148
+
149
+ # ⭐ بررسی خطای Rate Limit
150
+ if "429" in error_str or "Too Many Requests" in error_str or response.status_code == 429:
151
+ # محاسبه زمان انتظار با exponential backoff
152
+ backoff = min(
153
+ INITIAL_BACKOFF * (BACKOFF_MULTIPLIER ** attempt),
154
+ MAX_BACKOFF
155
+ )
156
+ # ⭐ اضافه کردن jitter تصادفی
157
+ jitter = random.uniform(0, backoff * 0.2)
158
+ wait_time = backoff + jitter
159
+
160
+ print(f" ⚠️ Rate Limit! تلاش {attempt + 1}/{MAX_RETRIES}")
161
+ print(f" ⏳ انتظار {wait_time:.1f} ثانیه...")
162
+ time.sleep(wait_time)
163
+
164
+ if attempt == MAX_RETRIES - 1:
165
+ raise Exception(f"خطا در ارتباط با Cerebras API پس از {MAX_RETRIES} تلاش: Rate Limit")
166
+
167
+ # ⭐ خطای 503 (Service Unavailable)
168
+ elif "503" in error_str or "Service Unavailable" in error_str:
169
+ wait_time = INITIAL_BACKOFF * (attempt + 1)
170
+ print(f" ⚠️ سرویس موقتاً در دسترس نیست. تلاش {attempt + 1}/{MAX_RETRIES}")
171
+ print(f" ⏳ انتظار {wait_time:.1f} ثانیه...")
172
+ time.sleep(wait_time)
173
+
174
+ if attempt == MAX_RETRIES - 1:
175
+ raise Exception(f"خطا: سرویس در دسترس نیست پس از {MAX_RETRIES} تلاش")
176
+
177
+ # ⭐ خطای timeout
178
+ elif "timeout" in error_str.lower() or "timed out" in error_str.lower():
179
+ wait_time = 5 * (attempt + 1)
180
+ print(f" ⚠️ Timeout! تلاش {attempt + 1}/{MAX_RETRIES}")
181
+ time.sleep(wait_time)
182
+
183
+ if attempt == MAX_RETRIES - 1:
184
+ raise Exception(f"خطا: Timeout پس از {MAX_RETRIES} تلاش")
185
+
186
+ else:
187
+ # سایر خطاها
188
+ if attempt < MAX_RETRIES - 1:
189
+ print(f" ⚠️ خطا: {error_str[:80]}... تلاش مجدد...")
190
+ time.sleep(INITIAL_BACKOFF)
191
+ else:
192
+ raise Exception(f"خطا در ارتباط با Cerebras API: {error_str}")
193
+
194
+ raise Exception(f"ناموفق پس از {MAX_RETRIES} تلاش")
195
+
196
+ def _clean_markdown(self, content: str) -> str:
197
+ """پاک کردن markdown از پاسخ"""
198
+ if "```" in content:
199
+ lines = content.split('\n')
200
+ clean_lines = []
201
+ skip = False
202
+ for line in lines:
203
+ if line.strip().startswith('```'):
204
+ skip = not skip
205
+ continue
206
+ if not skip:
207
+ clean_lines.append(line)
208
+ content = '\n'.join(clean_lines)
209
+ return content
210
+
211
+ def _analyze_anonymized_text(self, text: str) -> Dict[str, Any]:
212
+ """تحلیل متن ناشناس‌سازی شده"""
213
+ companies = re.findall(r'company-(\d+)', text)
214
+ persons = re.findall(r'person-(\d+)', text)
215
+ amounts = re.findall(r'amount-(\d+)', text)
216
+ percents = re.findall(r'percent-(\d+)', text)
217
+
218
+ statistics = {
219
+ "company": len(set(companies)),
220
+ "person": len(set(persons)),
221
+ "amount": len(set(amounts)),
222
+ "percent": len(set(percents)),
223
+ "total_replacements": len(companies) + len(persons) + len(amounts) + len(percents)
224
+ }
225
+
226
+ entities = {
227
+ "companies": sorted(list(set(companies)), key=lambda x: int(x)),
228
+ "persons": sorted(list(set(persons)), key=lambda x: int(x)),
229
+ "amounts": sorted(list(set(amounts)), key=lambda x: int(x)),
230
+ "percents": sorted(list(set(percents)), key=lambda x: int(x))
231
+ }
232
+
233
+ detailed_analysis = {
234
+ "preserved_dates": len(re.findall(r'\d{4}/\d{1,2}/\d{1,2}|\d{1,2}\s+\w+\s+\d{4}', text)),
235
+ "preserved_times": len(re.findall(r'\d{1,2}:\d{2}', text)),
236
+ "financial_indicators": len(re.findall(r'\b(EPS|P/E|ARPU|NPL|ROE|ROA)\b', text)),
237
+ "units_preserved": len(re.findall(r'(میلیارد|میلیون|هزار|تومان|ریال|درهم|دلار|یورو|تن|کیلوگرم)', text))
238
+ }
239
+
240
+ return {
241
+ "statistics": statistics,
242
+ "entities": entities,
243
+ "detailed_analysis": detailed_analysis
244
+ }
245
+
246
+ def _validate_anonymized_text(self, text: str) -> Dict[str, Any]:
247
+ """اعتبارسنجی پیشرفته متن ناشناس‌شده"""
248
+ companies = re.findall(r'company-(\d+)', text)
249
+ persons = re.findall(r'person-(\d+)', text)
250
+ amounts = re.findall(r'amount-(\d+)', text)
251
+ percents = re.findall(r'percent-(\d+)', text)
252
+
253
+ validation_issues = []
254
+
255
+ for entity_type, indices in [
256
+ ("company", companies),
257
+ ("person", persons),
258
+ ("amount", amounts),
259
+ ("percent", percents)
260
+ ]:
261
+ if indices:
262
+ unique_indices = sorted(list(set([int(x) for x in indices])))
263
+ if unique_indices[0] != 1:
264
+ validation_issues.append(f"اندیس {entity_type} از 01 شروع نشده")
265
+ expected = list(range(1, len(unique_indices) + 1))
266
+ if unique_indices != expected:
267
+ validation_issues.append(f"اندیس‌های {entity_type} پیوسته نیستند")
268
+
269
+ return {
270
+ "is_valid": len(validation_issues) == 0,
271
+ "issues": validation_issues,
272
+ "entity_counts": {
273
+ "company": len(set(companies)),
274
+ "person": len(set(persons)),
275
+ "amount": len(set(amounts)),
276
+ "percent": len(set(percents))
277
+ }
278
+ }
279
+
280
+ def anonymize_text(self, text: str) -> Dict[str, Any]:
281
+ """ناشناس‌سازی متن با استفاده از Cerebras"""
282
+ if not text or not text.strip():
283
+ return {
284
+ "success": False,
285
+ "error": "متن ورودی خالی است"
286
+ }
287
+
288
+ try:
289
+ response = self._make_api_request(text)
290
+
291
+ if "choices" not in response or not response["choices"]:
292
+ return {
293
+ "success": False,
294
+ "error": "پاسخ نامعتبر از API"
295
+ }
296
+
297
+ content = response["choices"][0]["message"]["content"]
298
+ content = self._clean_markdown(content)
299
+ content = content.strip()
300
+
301
+ analysis = self._analyze_anonymized_text(content)
302
+
303
+ return {
304
+ "success": True,
305
+ "anonymized_text": content,
306
+ "entities": analysis["entities"],
307
+ "statistics": analysis["statistics"],
308
+ "detailed_analysis": analysis["detailed_analysis"],
309
+ "usage": response.get("usage", {}),
310
+ "quality_check": self._validate_anonymized_text(content)
311
+ }
312
+
313
+ except Exception as e:
314
+ return {
315
+ "success": False,
316
+ "error": f"خطا در پردازش: {str(e)}"
317
+ }
318
+
319
+ def anonymize_batch(self, texts: List[str], progress_callback=None) -> List[Dict[str, Any]]:
320
+ """ناشناس‌سازی دسته‌ای متون"""
321
+ results = []
322
+ total = len(texts)
323
+
324
+ for idx, text in enumerate(texts):
325
+ if progress_callback:
326
+ progress_callback((idx + 1) / total, f"پردازش سطر {idx + 1} از {total}")
327
+
328
+ result = self.anonymize_text(text)
329
+ results.append(result)
330
+
331
+ # تاخیر برای جلوگیری از rate limiting
332
+ if idx < total - 1:
333
+ time.sleep(DELAY_BETWEEN_REQUESTS)
334
+
335
+ return results
336
+
337
+
338
+ # ============= توابع Checkpoint =============
339
+ def save_checkpoint(checkpoint_path: str, data: dict):
340
+ """ذخیره checkpoint"""
341
+ with open(checkpoint_path, 'w', encoding='utf-8') as f:
342
+ json.dump(data, f, ensure_ascii=False, indent=2)
343
+ print(f" 💾 Checkpoint ذخیره شد")
344
+
345
+ def load_checkpoint(checkpoint_path: str) -> dict:
346
+ """بارگذاری checkpoint"""
347
+ if Path(checkpoint_path).exists():
348
+ with open(checkpoint_path, 'r', encoding='utf-8') as f:
349
+ return json.load(f)
350
+ return None
351
+
352
+
353
+ def create_advanced_interface():
354
+ """ایجاد رابط کاربری پیشرفته با قابلیت پردازش دسته‌ای"""
355
+
356
+ api_key_available = bool(os.getenv("CEREBRAS_API_KEY"))
357
+
358
+ custom_css = """
359
+ .gradio-container {
360
+ font-family: 'Tahoma', 'Arial', sans-serif !important;
361
+ direction: rtl;
362
+ max-width: 1400px;
363
+ margin: 0 auto;
364
+ }
365
+ .result-box {
366
+ background-color: #f8f9fa;
367
+ border: 2px solid #e9ecef;
368
+ border-radius: 12px;
369
+ padding: 20px;
370
+ margin: 10px 0;
371
+ }
372
+ .warning-box {
373
+ background-color: #fff3cd;
374
+ border: 2px solid #ffeaa7;
375
+ border-radius: 12px;
376
+ padding: 15px;
377
+ color: #856404;
378
+ margin: 10px 0;
379
+ }
380
+ .success-box {
381
+ background-color: #d4edda;
382
+ border: 2px solid #c3e6cb;
383
+ border-radius: 12px;
384
+ padding: 15px;
385
+ color: #155724;
386
+ margin: 10px 0;
387
+ }
388
+ .batch-progress {
389
+ background-color: #e3f2fd;
390
+ border: 2px solid #90caf9;
391
+ border-radius: 12px;
392
+ padding: 15px;
393
+ margin: 10px 0;
394
+ }
395
+ """
396
+
397
+ with gr.Blocks(css=custom_css, title="ناشناس‌ساز پیشرفته متن فارسی با Cerebras", theme=gr.themes.Soft()) as interface:
398
+
399
+ gr.Markdown("""
400
+ # 🔒 سیستم پیشرفته ناشناس‌سازی متون مالی/خبری فارسی
401
+ ### ⚡ قدرت‌گرفته از Cerebras AI - با مدیریت هوشمند Rate Limit
402
+ """)
403
+
404
+ # API Key input
405
+ if api_key_available:
406
+ gr.Markdown("""
407
+ <div class="success-box">
408
+ ✅ <strong>سیستم آماده است</strong> - کلید API تنظیم شده
409
+ </div>
410
+ """)
411
+ api_key_input = gr.Textbox(visible=False, value="")
412
+ else:
413
+ gr.Markdown("""
414
+ <div class="warning-box">
415
+ ⚠️ <strong>کلید API تنظیم نشده</strong><br>
416
+ لطفاً کلید Cerebras API خود را در زیر وارد کنید
417
+ </div>
418
+ """)
419
+ api_key_input = gr.Textbox(
420
+ label="🔑 کلید Cerebras API",
421
+ placeholder="csk-...",
422
+ type="password"
423
+ )
424
+
425
+ # تب‌های اصلی
426
+ with gr.Tabs() as tabs:
427
+
428
+ # ===============================
429
+ # تب 1: پردازش تکی
430
+ # ===============================
431
+ with gr.TabItem("📝 پردازش تکی"):
432
+ with gr.Row():
433
+ with gr.Column(scale=1):
434
+ input_text = gr.Textbox(
435
+ label="📝 متن ورودی",
436
+ placeholder="متن مالی یا خبری خود را اینجا وارد کنید...",
437
+ lines=12,
438
+ rtl=True
439
+ )
440
+ with gr.Row():
441
+ anonymize_btn = gr.Button("🚀 ناشناس‌سازی", variant="primary", size="lg")
442
+ clear_btn = gr.Button("🗑️ پاک کردن", variant="secondary")
443
+
444
+ with gr.Column(scale=1):
445
+ output_text = gr.Textbox(
446
+ label="📤 متن ناشناس‌شده",
447
+ lines=12,
448
+ rtl=True,
449
+ interactive=False
450
+ )
451
+ copy_btn = gr.Button("📋 کپی نتیجه", variant="secondary")
452
+ copy_output = gr.Textbox(visible=False)
453
+
454
+ # آمار و تحلیل
455
+ with gr.Row():
456
+ with gr.Column():
457
+ statistics_output = gr.Markdown(label="📊 آمار")
458
+ with gr.Column():
459
+ quality_output = gr.Markdown(label="✅ کیفیت")
460
+
461
+ with gr.Row():
462
+ with gr.Column():
463
+ entities_output = gr.Markdown(label="🏷️ موجودیت‌ها")
464
+ with gr.Column():
465
+ detailed_analysis_output = gr.Markdown(label="📈 تحلیل تفصیلی")
466
+
467
+ usage_output = gr.Markdown(label="⚡ مصرف")
468
+
469
+ # ===============================
470
+ # تب 2: پردازش دسته‌ای
471
+ # ===============================
472
+ with gr.TabItem("📁 پردازش دسته‌ای"):
473
+ gr.Markdown("""
474
+ ### 📁 پردازش دسته‌ای فایل CSV
475
+ ⚠️ **توجه:** برای جلوگیری از خطای Rate Limit، بین هر درخواست **4 ثانیه** تاخیر اعمال می‌شود.
476
+
477
+ 💾 **قابلیت Checkpoint:** اگر پردازش قطع شود، می‌توانید از همان نقطه ادامه دهید.
478
+ """)
479
+
480
+ with gr.Row():
481
+ with gr.Column():
482
+ csv_file = gr.File(
483
+ label="📂 آپلود فایل CSV",
484
+ file_types=[".csv"]
485
+ )
486
+ text_column = gr.Dropdown(
487
+ label="📝 ستون متن",
488
+ choices=[],
489
+ interactive=True
490
+ )
491
+ output_column_name = gr.Textbox(
492
+ label="📤 نام ستون خروجی",
493
+ value="anonymized_text",
494
+ placeholder="anonymized_text"
495
+ )
496
+
497
+ # ⭐ گزینه ادامه از checkpoint
498
+ continue_from_checkpoint = gr.Checkbox(
499
+ label="📍 ادامه از checkpoint قبلی (اگر موجود باشد)",
500
+ value=True
501
+ )
502
+
503
+ batch_btn = gr.Button("🚀 شروع پردازش", variant="primary", size="lg")
504
+
505
+ with gr.Column():
506
+ preview_df = gr.Dataframe(
507
+ label="👁️ پیش‌نمایش فایل",
508
+ interactive=False,
509
+ wrap=True
510
+ )
511
+
512
+ batch_progress = gr.Markdown("📊 آماده پردازش...")
513
+ batch_stats = gr.Markdown("")
514
+
515
+ with gr.Row():
516
+ result_df = gr.Dataframe(
517
+ label="📊 نتایج پردازش",
518
+ interactive=False,
519
+ wrap=True
520
+ )
521
+
522
+ download_btn = gr.File(
523
+ label="📥 دانلود نتایج",
524
+ visible=False
525
+ )
526
+
527
+ error_log = gr.Markdown("")
528
+
529
+ # ===============================
530
+ # توابع پردازش
531
+ # ===============================
532
+
533
+ def load_csv_columns(file_path):
534
+ """بارگذاری ستون‌های CSV"""
535
+ if file_path is None:
536
+ return gr.Dropdown(choices=[]), None, "📊 فایل انتخاب نشده"
537
+
538
+ try:
539
+ try:
540
+ df = pd.read_csv(file_path, encoding='utf-8')
541
+ except:
542
+ try:
543
+ df = pd.read_csv(file_path, encoding='utf-8-sig')
544
+ except:
545
+ df = pd.read_csv(file_path, encoding='cp1256')
546
+
547
+ columns = df.columns.tolist()
548
+ preview = df.head(5)
549
+
550
+ return (
551
+ gr.Dropdown(choices=columns, value=columns[0] if columns else None),
552
+ preview,
553
+ f"✅ ��ایل بارگذاری شد | {len(df)} سطر | {len(columns)} ستون"
554
+ )
555
+ except Exception as e:
556
+ return gr.Dropdown(choices=[]), None, f"❌ خطا در بارگذاری: {str(e)}"
557
+
558
+ def process_single_text(text, api_key):
559
+ """پردازش تک متن"""
560
+ if not text or not text.strip():
561
+ return "", "⚠️ لطفاً متن وارد کنید", "", "", "", ""
562
+
563
+ try:
564
+ key = api_key if api_key else os.getenv("CEREBRAS_API_KEY")
565
+ if not key:
566
+ return "", "❌ کلید API تنظیم نشده", "", "", "", ""
567
+
568
+ anonymizer = AdvancedCerebrasAnonymizer(api_key=key)
569
+ result = anonymizer.anonymize_text(text)
570
+
571
+ if not result["success"]:
572
+ return "", f"❌ خطا: {result['error']}", "", "", "", ""
573
+
574
+ # آمار
575
+ stats = result["statistics"]
576
+ stats_md = f"""
577
+ ### 📊 آمار جایگزینی:
578
+ | نوع | تعداد |
579
+ |-----|-------|
580
+ | شرکت | {stats['company']} |
581
+ | شخص | {stats['person']} |
582
+ | مبلغ | {stats['amount']} |
583
+ | درصد | {stats['percent']} |
584
+ | **کل** | **{stats['total_replacements']}** |
585
+ """
586
+
587
+ # کیفیت
588
+ quality = result["quality_check"]
589
+ quality_status = "✅ معتبر" if quality["is_valid"] else "⚠️ مشکل دارد"
590
+ quality_md = f"### {quality_status}\n"
591
+ if quality["issues"]:
592
+ quality_md += "\n".join([f"- {issue}" for issue in quality["issues"]])
593
+
594
+ # موجودیت‌ها
595
+ entities = result["entities"]
596
+ entities_md = f"""
597
+ ### 🏷️ موجودیت‌های شناسایی شده:
598
+ - **شرکت‌ها:** {', '.join([f'company-{x}' for x in entities['companies']]) or '-'}
599
+ - **اشخاص:** {', '.join([f'person-{x}' for x in entities['persons']]) or '-'}
600
+ - **مبالغ:** {', '.join([f'amount-{x}' for x in entities['amounts']]) or '-'}
601
+ - **درصدها:** {', '.join([f'percent-{x}' for x in entities['percents']]) or '-'}
602
+ """
603
+
604
+ # تحلیل تفصیلی
605
+ detailed = result["detailed_analysis"]
606
+ detailed_md = f"""
607
+ | شاخص | مقدار |
608
+ |------|-------|
609
+ | تاریخ‌های حفظ شده | {detailed['preserved_dates']} |
610
+ | شاخص‌های مالی | {detailed['financial_indicators']} |
611
+ | واحدهای حفظ شده | {detailed['units_preserved']} |
612
+ """
613
+
614
+ # مصرف
615
+ usage = result.get("usage", {})
616
+ usage_md = f"⚡ **توکن‌ها:** ورودی: {usage.get('prompt_tokens', '-')} | خروجی: {usage.get('completion_tokens', '-')}"
617
+
618
+ return (
619
+ result["anonymized_text"],
620
+ stats_md,
621
+ quality_md,
622
+ entities_md,
623
+ detailed_md,
624
+ usage_md
625
+ )
626
+
627
+ except Exception as e:
628
+ return "", f"❌ خطا: {str(e)}", "", "", "", ""
629
+
630
+ def process_batch_csv(file_path, text_col, output_col, api_key, use_checkpoint, progress=gr.Progress()):
631
+ """پردازش دسته‌ای فایل CSV با Checkpoint"""
632
+ if file_path is None:
633
+ return None, "❌ لطفاً فایل CSV آپلود کنید", "", gr.File(visible=False), None
634
+
635
+ if not text_col:
636
+ return None, "❌ لطفاً ستون متن را انتخاب کنید", "", gr.File(visible=False), None
637
+
638
+ try:
639
+ # خواندن فایل
640
+ try:
641
+ df = pd.read_csv(file_path, encoding='utf-8')
642
+ except:
643
+ try:
644
+ df = pd.read_csv(file_path, encoding='utf-8-sig')
645
+ except:
646
+ df = pd.read_csv(file_path, encoding='cp1256')
647
+
648
+ if text_col not in df.columns:
649
+ return None, f"❌ ستون '{text_col}' در فایل یافت نشد", "", gr.File(visible=False), None
650
+
651
+ # محدودیت تعداد سطرها
652
+ max_rows = 1000
653
+ if len(df) > max_rows:
654
+ return None, f"❌ تعداد سطرها ({len(df)}) از حداکثر مجاز ({max_rows}) بیشتر است", "", gr.File(visible=False), None
655
+
656
+ # ایجاد anonymizer
657
+ key = api_key if api_key else os.getenv("CEREBRAS_API_KEY")
658
+ if not key:
659
+ return None, "❌ کلید API تنظیم نشده", "", gr.File(visible=False), None
660
+
661
+ anonymizer = AdvancedCerebrasAnonymizer(api_key=key)
662
+
663
+ # ⭐ بررسی checkpoint
664
+ checkpoint_path = "/tmp/anonymizer_checkpoint.json"
665
+ start_index = 0
666
+ anonymized_texts = [""] * len(df)
667
+ error_rows = []
668
+ success_count = 0
669
+
670
+ if use_checkpoint:
671
+ checkpoint = load_checkpoint(checkpoint_path)
672
+ if checkpoint:
673
+ start_index = checkpoint.get('last_processed', -1) + 1
674
+ anonymized_texts = checkpoint.get('results', [""] * len(df))
675
+ success_count = checkpoint.get('success_count', 0)
676
+ error_rows = checkpoint.get('errors', [])
677
+ print(f"✅ ادامه از checkpoint - ردیف {start_index}")
678
+ progress(start_index / len(df), desc=f"ادامه از ردیف {start_index}...")
679
+
680
+ # پردازش سطرها
681
+ total = len(df)
682
+
683
+ progress(start_index / total, desc="شروع پردازش...")
684
+
685
+ for idx in range(start_index, total):
686
+ row = df.iloc[idx]
687
+ text = str(row[text_col])
688
+
689
+ progress((idx + 1) / total, desc=f"پردازش سطر {idx + 1} از {total} (⏱️ ~{DELAY_BETWEEN_REQUESTS}s/row)")
690
+
691
+ if not text or text.strip() == '' or text.lower() == 'nan':
692
+ anonymized_texts[idx] = ""
693
+ continue
694
+
695
+ result = anonymizer.anonymize_text(text)
696
+
697
+ if result["success"]:
698
+ anonymized_texts[idx] = result["anonymized_text"]
699
+ success_count += 1
700
+ else:
701
+ anonymized_texts[idx] = f"[خطا: {result['error']}]"
702
+ error_rows.append(f"سطر {idx + 1}: {result['error']}")
703
+
704
+ # ⭐ ذخیره checkpoint هر چند ردیف
705
+ if (idx + 1) % CHECKPOINT_INTERVAL == 0:
706
+ save_checkpoint(checkpoint_path, {
707
+ 'last_processed': idx,
708
+ 'results': anonymized_texts,
709
+ 'success_count': success_count,
710
+ 'errors': error_rows,
711
+ 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S')
712
+ })
713
+
714
+ # ⭐ تاخیر برای جلوگیری از rate limit (اعمال شده در _make_api_request)
715
+
716
+ # ⭐ ذخیره checkpoint نهایی
717
+ save_checkpoint(checkpoint_path, {
718
+ 'last_processed': total - 1,
719
+ 'results': anonymized_texts,
720
+ 'success_count': success_count,
721
+ 'errors': error_rows,
722
+ 'completed': True,
723
+ 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S')
724
+ })
725
+
726
+ # اضافه کردن ستون جدید
727
+ output_col_name = output_col if output_col else "anonymized_text"
728
+ df[output_col_name] = anonymized_texts
729
+
730
+ # ذخیره فایل خروجی
731
+ output_path = "/tmp/anonymized_output.csv"
732
+ df.to_csv(output_path, index=False, encoding='utf-8-sig')
733
+
734
+ # آمار
735
+ stats_md = f"""
736
+ ### 📊 آمار پردازش:
737
+ | شاخص | مقدار |
738
+ |------|-------|
739
+ | کل سطرها | {total} |
740
+ | پردازش موفق | {success_count} |
741
+ | خطا | {len(error_rows)} |
742
+ | درصد موفقیت | {(success_count/total*100):.1f}% |
743
+ | تاخیر بین درخواست‌ها | {DELAY_BETWEEN_REQUESTS} ثانیه |
744
+ """
745
+
746
+ # گزارش خطاها
747
+ error_md = ""
748
+ if error_rows:
749
+ error_md = "### ⚠️ خطاهای مشاهده شده:\n" + "\n".join([f"- {e}" for e in error_rows[:20]])
750
+ if len(error_rows) > 20:
751
+ error_md += f"\n... و {len(error_rows) - 20} خطای دیگر"
752
+
753
+ # نمایش نتایج
754
+ result_preview = df[[text_col, output_col_name]].head(10)
755
+
756
+ return (
757
+ result_preview,
758
+ f"✅ **پردازش کامل شد!** | {success_count} سطر با موفقیت",
759
+ stats_md,
760
+ gr.File(value=output_path, visible=True),
761
+ error_md
762
+ )
763
+
764
+ except Exception as e:
765
+ return None, f"❌ خطا در پردازش: {str(e)}", "", gr.File(visible=False), str(e)
766
+
767
+ def copy_text(text_to_copy):
768
+ """کپی متن"""
769
+ if not text_to_copy or not text_to_copy.strip():
770
+ return gr.Textbox(visible=False), "⚠️ متنی برای کپی وجود ندارد"
771
+ return gr.Textbox(value=text_to_copy, visible=True), "✅ متن کپی شد"
772
+
773
+ def clear_all():
774
+ """پاک کردن فیلدها"""
775
+ return "", "", "", "", "", "", "", gr.Textbox(visible=False)
776
+
777
+ # ===============================
778
+ # اتصال رویدادها
779
+ # ===============================
780
+
781
+ # پردازش تکی
782
+ anonymize_btn.click(
783
+ fn=process_single_text,
784
+ inputs=[input_text, api_key_input],
785
+ outputs=[output_text, statistics_output, quality_output, entities_output, detailed_analysis_output, usage_output]
786
+ )
787
+
788
+ copy_btn.click(
789
+ fn=copy_text,
790
+ inputs=[output_text],
791
+ outputs=[copy_output, statistics_output]
792
+ )
793
+
794
+ clear_btn.click(
795
+ fn=clear_all,
796
+ outputs=[input_text, output_text, statistics_output, quality_output, entities_output, detailed_analysis_output, usage_output, copy_output]
797
+ )
798
+
799
+ # پردازش دسته‌ای
800
+ csv_file.change(
801
+ fn=load_csv_columns,
802
+ inputs=[csv_file],
803
+ outputs=[text_column, preview_df, batch_progress]
804
+ )
805
+
806
+ batch_btn.click(
807
+ fn=process_batch_csv,
808
+ inputs=[csv_file, text_column, output_column_name, api_key_input, continue_from_checkpoint],
809
+ outputs=[result_df, batch_progress, batch_stats, download_btn, error_log]
810
+ )
811
+
812
+ # مثال‌ها
813
+ gr.Examples(
814
+ examples=[
815
+ ["مهدی اخوان بهابادی، مدیرعامل همراه اول، اعلام کرد درآمد عملیاتی شرکت با رشد 37 درصدی به 70 هزار و 677 میلیارد تومان رسیده است."],
816
+ ["بانک مرکزی و بانک ملی با همکاری محمدرضا فرزین، 60 درصد سپرده‌ها را مدیریت کردند."],
817
+ ["سازمان تامین اجتماعی دارای سه شرکت دارویی است که از مراکز درمانی وابسته به وزارت بهداشت مطالباتی دارند."]
818
+ ],
819
+ inputs=input_text,
820
+ label="📚 مثال‌ها"
821
+ )
822
+
823
+ return interface
824
+
825
+
826
+ # اجرای برنامه
827
+ if __name__ == "__main__":
828
+ interface = create_advanced_interface()
829
+ interface.launch(
830
+ server_name="0.0.0.0",
831
+ server_port=7860,
832
+ share=True,
833
+ show_error=True
834
+ )