leilaghomashchi commited on
Commit
6ab9ece
·
verified ·
1 Parent(s): 8dd412b

Upload app_batch_anonymizer_v2 (4).py

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