leilaghomashchi commited on
Commit
fe0e549
·
verified ·
1 Parent(s): 3511f32

Upload app_improved (1).py

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