leilaghomashchi commited on
Commit
b99d576
·
verified ·
1 Parent(s): c2813a8

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -779
app.py DELETED
@@ -1,779 +0,0 @@
1
- import requests
2
- import json
3
- import gradio as gr
4
- from typing import Dict, Any, List, Generator, Optional
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
- from enum import Enum
17
-
18
- class APIProvider(Enum):
19
- """پرووایدرهای API پشتیبانی شده"""
20
- CEREBRAS = "cerebras"
21
- GROQ = "groq"
22
- OPENAI = "openai"
23
-
24
- @dataclass
25
- class APIConfig:
26
- """تنظیمات API"""
27
- provider: APIProvider
28
- api_key: str
29
- base_url: str
30
- model: str
31
- max_tokens: int = 2000
32
- temperature: float = 0.1
33
-
34
- # تنظیمات پیش‌فرض برای هر پرووایدر
35
- API_CONFIGS = {
36
- APIProvider.CEREBRAS: {
37
- "base_url": "https://api.cerebras.ai/v1",
38
- "models": ["qwen-3-32b", "llama-4-scout-17b-16e-instruct", "llama3.1-8b"],
39
- "default_model": "qwen-3-32b"
40
- },
41
- APIProvider.GROQ: {
42
- "base_url": "https://api.groq.com/openai/v1",
43
- "models": ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768"],
44
- "default_model": "llama-3.3-70b-versatile"
45
- },
46
- APIProvider.OPENAI: {
47
- "base_url": "https://api.openai.com/v1",
48
- "models": ["gpt-4o-mini", "gpt-4o", "gpt-3.5-turbo"],
49
- "default_model": "gpt-4o-mini"
50
- }
51
- }
52
-
53
- @dataclass
54
- class RateLimitConfig:
55
- """تنظیمات محدودیت نرخ درخواست"""
56
- requests_per_minute: int = 30
57
- min_delay_between_requests: float = 2.0
58
- max_retries: int = 5
59
- initial_backoff: float = 5.0
60
- max_backoff: float = 60.0
61
- backoff_multiplier: float = 2.0
62
- recovery_window: float = 60.0
63
-
64
- class RateLimiter:
65
- """مدیریت محدودیت نرخ درخواست"""
66
-
67
- def __init__(self, config: RateLimitConfig):
68
- self.config = config
69
- self.request_times: List[float] = []
70
- self.lock = threading.Lock()
71
- self.consecutive_failures = 0
72
- self.last_failure_time = 0
73
- self.last_success_time = time.time()
74
- self.total_429_errors = 0
75
-
76
- def wait_if_needed(self) -> float:
77
- """انتظار تا زمان مجاز ارسال درخواست بعدی"""
78
- with self.lock:
79
- now = time.time()
80
-
81
- # پاک کردن درخواست‌های قدیمی
82
- self.request_times = [t for t in self.request_times if now - t < 60]
83
-
84
- # بازیابی از خطاهای قبلی
85
- if self.consecutive_failures > 0 and (now - self.last_failure_time) > self.config.recovery_window:
86
- self.consecutive_failures = max(0, self.consecutive_failures - 1)
87
-
88
- wait_time = 0.0
89
-
90
- # محدودیت درخواست در دقیقه
91
- if len(self.request_times) >= self.config.requests_per_minute:
92
- oldest_request = min(self.request_times)
93
- wait_time = max(wait_time, 60 - (now - oldest_request) + 1)
94
-
95
- # حداقل تأخیر
96
- if self.request_times:
97
- time_since_last = now - max(self.request_times)
98
- if time_since_last < self.config.min_delay_between_requests:
99
- wait_time = max(wait_time, self.config.min_delay_between_requests - time_since_last)
100
-
101
- # backoff برای خطاها
102
- if self.consecutive_failures > 0:
103
- failure_wait = min(
104
- self.config.initial_backoff * (self.config.backoff_multiplier ** min(self.consecutive_failures, 4)),
105
- self.config.max_backoff
106
- )
107
- wait_time = max(wait_time, failure_wait)
108
-
109
- if wait_time > 0:
110
- time.sleep(wait_time)
111
-
112
- self.request_times.append(time.time())
113
- return wait_time
114
-
115
- def report_success(self):
116
- with self.lock:
117
- self.last_success_time = time.time()
118
- self.consecutive_failures = 0
119
-
120
- def report_failure(self, is_rate_limit: bool = False):
121
- with self.lock:
122
- self.last_failure_time = time.time()
123
- if is_rate_limit:
124
- self.consecutive_failures += 1
125
- self.total_429_errors += 1
126
- else:
127
- self.consecutive_failures = min(self.consecutive_failures + 0.5, 3)
128
-
129
- def get_estimated_wait_time(self) -> float:
130
- with self.lock:
131
- now = time.time()
132
- self.request_times = [t for t in self.request_times if now - t < 60]
133
- if len(self.request_times) >= self.config.requests_per_minute:
134
- return max(0, 60 - (now - min(self.request_times)) + 1)
135
- return self.config.min_delay_between_requests
136
-
137
- def get_status(self) -> Dict[str, Any]:
138
- with self.lock:
139
- return {
140
- "consecutive_failures": self.consecutive_failures,
141
- "total_429_errors": self.total_429_errors,
142
- "requests_in_last_minute": len(self.request_times)
143
- }
144
-
145
-
146
- class MultiProviderAnonymizer:
147
- """سیستم ناشناس‌سازی با پشتیبانی از چند API"""
148
-
149
- def __init__(
150
- self,
151
- provider: APIProvider,
152
- api_key: str,
153
- model: str = None,
154
- rate_limit_config: RateLimitConfig = None
155
- ):
156
- self.provider = provider
157
- self.api_key = api_key
158
-
159
- provider_config = API_CONFIGS[provider]
160
- self.base_url = provider_config["base_url"]
161
- self.model = model or provider_config["default_model"]
162
-
163
- self.rate_limit_config = rate_limit_config or RateLimitConfig()
164
- self.rate_limiter = RateLimiter(self.rate_limit_config)
165
- self.system_prompt = self._create_system_prompt()
166
- self.session = self._create_session()
167
-
168
- def _create_session(self) -> requests.Session:
169
- """ایجاد session با تنظیمات بهینه"""
170
- session = requests.Session()
171
- retry_strategy = Retry(
172
- total=3,
173
- backoff_factor=1,
174
- status_forcelist=[500, 502, 503, 504],
175
- allowed_methods=["POST"]
176
- )
177
- adapter = HTTPAdapter(max_retries=retry_strategy, pool_connections=10, pool_maxsize=10)
178
- session.mount("https://", adapter)
179
- session.mount("http://", adapter)
180
- return session
181
-
182
- def _remove_thinking_tags(self, text: str) -> str:
183
- """حذف تگ‌های thinking از خروجی مدل‌های reasoning مثل Qwen"""
184
- if not text:
185
- return text
186
-
187
- # حذف تگ‌های <think>...</think> و <thinking>...</thinking>
188
- # با پشتیبانی از حروف بزرگ/کوچک و چند خطی
189
- patterns = [
190
- r'<think>.*?</think>',
191
- r'<thinking>.*?</thinking>',
192
- r'<Think>.*?</Think>',
193
- r'<Thinking>.*?</Thinking>',
194
- ]
195
-
196
- for pattern in patterns:
197
- text = re.sub(pattern, '', text, flags=re.DOTALL)
198
-
199
- # اگر تگ باز بود ولی بسته نشد، همه چیز بعدش را حذف کن
200
- for tag in ['<think>', '<Think>', '<thinking>', '<Thinking>']:
201
- if tag.lower() in text.lower():
202
- # پیدا کردن موقعیت تگ و حذف از آنجا تا انتها یا تا تگ بسته
203
- idx = text.lower().find(tag.lower())
204
- if idx != -1:
205
- # چک کن آیا تگ بسته شدن وجود داره
206
- close_tag = tag.replace('<', '</')
207
- close_idx = text.lower().find(close_tag.lower(), idx)
208
- if close_idx != -1:
209
- # حذف از تگ باز تا بعد از تگ بسته
210
- text = text[:idx] + text[close_idx + len(close_tag):]
211
- else:
212
- # تگ بسته نشده، حذف از تگ باز تا انتها
213
- text = text[:idx]
214
-
215
- return text.strip()
216
-
217
- def _create_system_prompt(self) -> str:
218
- """دستورالعمل سیستمی"""
219
- return """شما یک «ناشناس‌ساز متون مالی/خبری فارسی» هستید. وظیفه‌تان جایگزینی اسامی خاص و مقادیر عددی با شناسه‌های بی‌معناست.
220
-
221
- ## **مهم: بدون توضیح**
222
- - فقط متن ناشناس‌شده را برگردان
223
- - هیچ توضیحی ندهید
224
- - از تگ‌های <think> یا <thinking> استفاده نکنید
225
- - فقط خروجی نهایی
226
-
227
- ## **قوانین اندیس‌گذاری**
228
- - شرکت‌ها: company-01, company-02, ... (پیوسته)
229
- - اشخاص: person-01, person-02, ... (پیوسته)
230
- - مبالغ: amount-01, amount-02, ... (پیوسته)
231
- - درصدها: percent-01, percent-02, ... (پیوسته)
232
-
233
- ## **ثبات شناسه‌ها:**
234
- - اگر "همراه اول" اول‌بار company-01 شد، در تمام متن همان باشد
235
-
236
- ## **قانون مهم برای مبالغ:**
237
- - **عدد + واحد = یک amount**: عدد به همراه واحد پولی/مقداری باید با هم یک توکن amount شوند
238
- - مثال‌ها:
239
- - 230 هزارمیلیارد → amount-01
240
- - 500 میلیون → amount-02
241
- - 45 هزارمیلیون → amount-03
242
- - 120 میلیارد تومان → amount-04
243
- - 80 همت → amount-05
244
- - 2.5 تریلیون → amount-06
245
- - واحدهای پولی شامل: میلیون، میلیارد، هزارمیلیارد، هزارمیلیون، تریلیون، همت، تومان، ریال
246
-
247
- ## **تشخیص صحیح:**
248
- - **شرکت/سازمان:** همراه اول، بانک ملی، ایران‌خودرو، سایپا، بانک مرکزی
249
- - **گروه‌ها:** "گروه همراه اول" → company-XX (نه group-XX)
250
- - **کلمات عمومی:** "سه شرکت"، "چند بانک" → حفظ شوند
251
- - **شخص:** مهدی اخوان بهابادی، محمدرضا فرزین
252
- - **درصد:** 37 درصدی، 15%
253
-
254
- ## **مثال:**
255
- **ورودی:** مهدی اخوان، مدیرعامل همراه اول، اعلام کرد درآمد با رشد 37 درصدی به 70 میلیارد تومان رسید.
256
- **خروجی:** person-01، مدیرعامل company-01، اعلام کرد درآمد با رشد percent-01 به amount-01 رسید.
257
-
258
- **ورودی:** سود خالص از 230 هزارمیلیارد به 500 میلیارد افزایش یافت.
259
- **خروجی:** سود خالص از amount-01 به amount-02 افزایش یافت.
260
-
261
- ## **حفظ شود:**
262
- - تاریخ‌ها: 1404/04/23
263
- - دوره‌های زمانی: ۹ ماهه، ۵ ماهه سال
264
- - مکان‌ها: تهران، اصفهان
265
-
266
- **فقط متن ناشناس‌شده را برگردان - بدون توضیح اضافی.**
267
- """
268
-
269
- def _make_api_request(self, text: str) -> Dict[str, Any]:
270
- """ارسال درخواست به API"""
271
- headers = {
272
- "Authorization": f"Bearer {self.api_key}",
273
- "Content-Type": "application/json"
274
- }
275
-
276
- payload = {
277
- "messages": [
278
- {"role": "system", "content": self.system_prompt},
279
- {"role": "user", "content": text}
280
- ],
281
- "model": self.model,
282
- "temperature": 0.1,
283
- "max_tokens": 2000
284
- }
285
-
286
- # اضافه کردن پارامترهای خاص هر پرووایدر
287
- if self.provider == APIProvider.GROQ:
288
- # Groq نیاز به این پارامتر نداره
289
- pass
290
-
291
- last_error = None
292
-
293
- for attempt in range(self.rate_limit_config.max_retries):
294
- self.rate_limiter.wait_if_needed()
295
-
296
- try:
297
- response = self.session.post(
298
- f"{self.base_url}/chat/completions",
299
- headers=headers,
300
- json=payload,
301
- timeout=90
302
- )
303
-
304
- if response.status_code == 429:
305
- self.rate_limiter.report_failure(is_rate_limit=True)
306
- retry_after = response.headers.get('Retry-After')
307
- wait_seconds = int(retry_after) if retry_after else min(
308
- self.rate_limit_config.initial_backoff * (self.rate_limit_config.backoff_multiplier ** attempt),
309
- 60.0
310
- )
311
- last_error = f"Rate limit (429). تلاش {attempt + 1}/{self.rate_limit_config.max_retries}"
312
- print(f"🚫 {last_error} - انتظار {wait_seconds}s")
313
- time.sleep(wait_seconds)
314
- continue
315
-
316
- if response.status_code == 401:
317
- raise Exception(f"❌ کلید API نامعتبر است برای {self.provider.value}")
318
-
319
- if response.status_code == 503:
320
- self.rate_limiter.report_failure(is_rate_limit=False)
321
- last_error = f"سرویس در دسترس نیست (503)"
322
- time.sleep(10)
323
- continue
324
-
325
- response.raise_for_status()
326
- self.rate_limiter.report_success()
327
- return response.json()
328
-
329
- except requests.exceptions.Timeout:
330
- self.rate_limiter.report_failure(is_rate_limit=False)
331
- last_error = f"Timeout"
332
- time.sleep(5)
333
-
334
- except requests.exceptions.ConnectionError as e:
335
- self.rate_limiter.report_failure(is_rate_limit=False)
336
- last_error = f"خطای اتصال: {str(e)[:50]}"
337
- time.sleep(10)
338
-
339
- except requests.exceptions.RequestException as e:
340
- self.rate_limiter.report_failure(is_rate_limit=False)
341
- last_error = f"خطای شبکه: {str(e)[:50]}"
342
- time.sleep(5)
343
-
344
- raise Exception(f"ناموفق پس از {self.rate_limit_config.max_retries} تلاش: {last_error}")
345
-
346
- def anonymize_text(self, text: str) -> Dict[str, Any]:
347
- """ناشناس‌سازی متن"""
348
- if not text or not text.strip():
349
- return {"success": False, "error": "متن ورودی خالی است", "anonymized_text": ""}
350
-
351
- try:
352
- response = self._make_api_request(text)
353
-
354
- if "choices" not in response or not response["choices"]:
355
- return {"success": False, "error": "پاسخ نامعتبر", "anonymized_text": ""}
356
-
357
- content = response["choices"][0]["message"]["content"]
358
-
359
- # پاک کردن markdown
360
- if "```" in content:
361
- lines = content.split('\n')
362
- clean_lines = []
363
- skip = False
364
- for line in lines:
365
- if line.strip().startswith('```'):
366
- skip = not skip
367
- continue
368
- if not skip:
369
- clean_lines.append(line)
370
- content = '\n'.join(clean_lines)
371
-
372
- content = content.strip()
373
-
374
- # پاک کردن تگ‌های thinking (برای مدل‌های Qwen و مشابه)
375
- content = self._remove_thinking_tags(content)
376
- content = content.strip()
377
-
378
- # تحلیل نتایج
379
- companies = re.findall(r'company-(\d+)', content)
380
- persons = re.findall(r'person-(\d+)', content)
381
- amounts = re.findall(r'amount-(\d+)', content)
382
- percents = re.findall(r'percent-(\d+)', content)
383
-
384
- return {
385
- "success": True,
386
- "anonymized_text": content,
387
- "statistics": {
388
- "company": len(set(companies)),
389
- "person": len(set(persons)),
390
- "amount": len(set(amounts)),
391
- "percent": len(set(percents))
392
- },
393
- "usage": response.get("usage", {})
394
- }
395
-
396
- except Exception as e:
397
- return {"success": False, "error": f"خطا: {str(e)}", "anonymized_text": ""}
398
-
399
-
400
- class BatchProcessor:
401
- """پردازشگر دسته‌ای"""
402
-
403
- def __init__(
404
- self,
405
- provider: APIProvider,
406
- api_key: str,
407
- model: str = None,
408
- rate_limit_config: RateLimitConfig = None
409
- ):
410
- self.provider = provider
411
- self.api_key = api_key
412
- self.model = model
413
- self.rate_limit_config = rate_limit_config or RateLimitConfig()
414
- self.anonymizer = None
415
- self.is_cancelled = False
416
- self.current_progress = 0
417
- self.total_rows = 0
418
- self.processed_rows = 0
419
- self.failed_rows = 0
420
- self.start_time = None
421
- self.consecutive_api_failures = 0
422
- self.max_consecutive_failures = 10
423
-
424
- def cancel(self):
425
- self.is_cancelled = True
426
-
427
- def reset(self):
428
- self.is_cancelled = False
429
- self.current_progress = 0
430
- self.total_rows = 0
431
- self.processed_rows = 0
432
- self.failed_rows = 0
433
- self.start_time = None
434
- self.consecutive_api_failures = 0
435
-
436
- def process_csv(
437
- self,
438
- file_path: str,
439
- text_column: str,
440
- output_column: str = "anonymized_text"
441
- ) -> Generator[Dict[str, Any], None, None]:
442
- """پردازش فایل CSV"""
443
-
444
- self.reset()
445
- self.start_time = time.time()
446
-
447
- # خواندن فایل
448
- try:
449
- df = pd.read_csv(file_path, encoding='utf-8')
450
- except UnicodeDecodeError:
451
- try:
452
- df = pd.read_csv(file_path, encoding='utf-8-sig')
453
- except:
454
- df = pd.read_csv(file_path, encoding='cp1256')
455
-
456
- if text_column not in df.columns:
457
- yield {"type": "error", "message": f"ستون '{text_column}' یافت نشد"}
458
- return
459
-
460
- self.total_rows = len(df)
461
-
462
- # ایجاد anonymizer
463
- self.anonymizer = MultiProviderAnonymizer(
464
- provider=self.provider,
465
- api_key=self.api_key,
466
- model=self.model,
467
- rate_limit_config=self.rate_limit_config
468
- )
469
-
470
- df[output_column] = ""
471
- df["anonymization_status"] = ""
472
- df["entities_found"] = ""
473
-
474
- yield {
475
- "type": "info",
476
- "message": f"🚀 شروع پردازش {self.total_rows} ردیف با {self.provider.value}..."
477
- }
478
-
479
- for idx, row in df.iterrows():
480
- if self.is_cancelled:
481
- yield {
482
- "type": "cancelled",
483
- "message": "لغو شد",
484
- "processed": self.processed_rows,
485
- "failed": self.failed_rows
486
- }
487
- break
488
-
489
- if self.consecutive_api_failures >= self.max_consecutive_failures:
490
- partial_path = file_path.replace('.csv', f'_partial_{idx}.csv')
491
- df.to_csv(partial_path, index=False, encoding='utf-8-sig')
492
- yield {
493
- "type": "error",
494
- "message": f"❌ توقف - خطاهای متوالی زیاد. ذخیره شد: {partial_path}"
495
- }
496
- return
497
-
498
- text = str(row[text_column]) if pd.notna(row[text_column]) else ""
499
-
500
- if not text.strip():
501
- df.at[idx, output_column] = ""
502
- df.at[idx, "anonymization_status"] = "خالی"
503
- self.processed_rows += 1
504
- continue
505
-
506
- result = self.anonymizer.anonymize_text(text)
507
-
508
- if result["success"]:
509
- df.at[idx, output_column] = result["anonymized_text"]
510
- df.at[idx, "anonymization_status"] = "موفق"
511
- stats = result.get("statistics", {})
512
- df.at[idx, "entities_found"] = f"C:{stats.get('company',0)}|P:{stats.get('person',0)}|A:{stats.get('amount',0)}|%:{stats.get('percent',0)}"
513
- self.processed_rows += 1
514
- self.consecutive_api_failures = 0
515
- else:
516
- df.at[idx, output_column] = f"خطا: {result.get('error', '')}"
517
- df.at[idx, "anonymization_status"] = "ناموفق"
518
- self.failed_rows += 1
519
- if "rate limit" in result.get('error', '').lower() or "429" in result.get('error', ''):
520
- self.consecutive_api_failures += 1
521
-
522
- # پیشرفت
523
- self.current_progress = (idx + 1) / self.total_rows * 100
524
- elapsed = time.time() - self.start_time
525
-
526
- yield {
527
- "type": "progress",
528
- "current": idx + 1,
529
- "total": self.total_rows,
530
- "progress": self.current_progress,
531
- "processed": self.processed_rows,
532
- "failed": self.failed_rows,
533
- "elapsed": elapsed,
534
- "estimated_remaining": (elapsed / (idx + 1)) * (self.total_rows - idx - 1),
535
- "next_wait": self.anonymizer.rate_limiter.get_estimated_wait_time(),
536
- "last_result": result,
537
- "rate_status": self.anonymizer.rate_limiter.get_status()
538
- }
539
-
540
- # Checkpoint هر 100 ردیف
541
- if (idx + 1) % 100 == 0:
542
- checkpoint_path = file_path.replace('.csv', f'_checkpoint_{idx+1}.csv')
543
- df.to_csv(checkpoint_path, index=False, encoding='utf-8-sig')
544
- yield {"type": "info", "message": f"💾 Checkpoint: {checkpoint_path}"}
545
-
546
- # ذخیره نهایی
547
- if not self.is_cancelled:
548
- output_path = file_path.replace('.csv', '_anonymized.csv')
549
- if output_path == file_path:
550
- output_path = file_path + '_anonymized.csv'
551
-
552
- df.to_csv(output_path, index=False, encoding='utf-8-sig')
553
-
554
- yield {
555
- "type": "complete",
556
- "message": "✅ تکمیل شد!",
557
- "output_path": output_path,
558
- "total": self.total_rows,
559
- "processed": self.processed_rows,
560
- "failed": self.failed_rows,
561
- "total_time": time.time() - self.start_time
562
- }
563
-
564
-
565
- def create_interface():
566
- """ایجاد رابط کاربری"""
567
-
568
- custom_css = """
569
- .rtl-text { direction: rtl; text-align: right; font-family: 'Vazirmatn', 'Tahoma', sans-serif; }
570
- """
571
-
572
- with gr.Blocks(css=custom_css, title="ناشناس‌ساز چند-API", theme=gr.themes.Soft()) as interface:
573
- gr.Markdown("""
574
- # 🔒 سیستم ناشناس‌سازی متون فارسی
575
- ### پشتیبانی از Cerebras، Groq و OpenAI
576
- """, elem_classes=["rtl-text"])
577
-
578
- batch_processor = {"instance": None}
579
-
580
- with gr.Tabs():
581
- # تب پردازش تکی
582
- with gr.Tab("پردازش تکی"):
583
- with gr.Row():
584
- with gr.Column():
585
- single_provider = gr.Dropdown(
586
- label="🌐 پرووایدر API",
587
- choices=["cerebras", "groq", "openai"],
588
- value="groq",
589
- elem_classes=["rtl-text"]
590
- )
591
- single_api_key = gr.Textbox(
592
- label="🔑 کلید API",
593
- placeholder="کلید API خود را وارد کنید",
594
- type="password"
595
- )
596
- single_model = gr.Dropdown(
597
- label="🤖 مدل",
598
- choices=["llama-3.3-70b-versatile", "llama-3.1-8b-instant"],
599
- value="llama-3.3-70b-versatile"
600
- )
601
- single_input = gr.Textbox(
602
- label="متن ورودی",
603
- lines=5,
604
- elem_classes=["rtl-text"]
605
- )
606
- single_btn = gr.Button("🔄 ناشناس‌سازی", variant="primary")
607
-
608
- with gr.Column():
609
- single_output = gr.Textbox(label="متن ناشناس‌شده", lines=5, elem_classes=["rtl-text"])
610
- single_stats = gr.Textbox(label="آمار", lines=2)
611
-
612
- # تب پردازش دسته‌ای
613
- with gr.Tab("پردازش دسته‌ای"):
614
- with gr.Row():
615
- with gr.Column():
616
- batch_provider = gr.Dropdown(
617
- label="🌐 پرووایدر API",
618
- choices=["cerebras", "groq", "openai"],
619
- value="groq"
620
- )
621
- batch_api_key = gr.Textbox(
622
- label="🔑 کلید API",
623
- type="password"
624
- )
625
- batch_model = gr.Dropdown(
626
- label="🤖 مدل",
627
- choices=["llama-3.3-70b-versatile", "qwen-3-32b", "gpt-4o-mini"],
628
- value="llama-3.3-70b-versatile"
629
- )
630
- csv_file = gr.File(label="📁 فایل CSV", file_types=[".csv"])
631
- text_column = gr.Dropdown(label="ستون متن", choices=[], allow_custom_value=True)
632
- output_column = gr.Textbox(label="نام ستون خروجی", value="anonymized_text")
633
-
634
- with gr.Column():
635
- delay_slider = gr.Slider(1, 30, 3, step=0.5, label="تأخیر (ثانیه)")
636
- rpm_slider = gr.Slider(5, 30, 20, step=1, label="حداکثر درخواست/دقیقه")
637
- retries_slider = gr.Slider(1, 10, 5, step=1, label="تلاش مجدد")
638
-
639
- with gr.Row():
640
- start_btn = gr.Button("🚀 شروع", variant="primary", size="lg")
641
- cancel_btn = gr.Button("⏹️ لغو", variant="stop", size="lg")
642
-
643
- progress_bar = gr.Slider(0, 100, 0, label="پیشرفت", interactive=False)
644
- progress_text = gr.Markdown("در انتظار...")
645
- time_stats = gr.Markdown("")
646
- process_log = gr.Textbox(lines=10, label="لاگ")
647
- preview_table = gr.Dataframe(headers=["متن", "ناشناس‌شده", "وضعیت"])
648
- output_file = gr.File(label="دانلود", visible=False)
649
-
650
- # توابع
651
- def update_models(provider):
652
- if provider == "cerebras":
653
- return gr.update(choices=["qwen-3-32b", "llama-4-scout-17b-16e-instruct"], value="qwen-3-32b")
654
- elif provider == "groq":
655
- return gr.update(choices=["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768"], value="llama-3.3-70b-versatile")
656
- else:
657
- return gr.update(choices=["gpt-4o-mini", "gpt-4o"], value="gpt-4o-mini")
658
-
659
- def update_columns(file):
660
- if file is None:
661
- return gr.update(choices=[], value=None)
662
- try:
663
- df = pd.read_csv(file.name, nrows=0, encoding='utf-8')
664
- except:
665
- try:
666
- df = pd.read_csv(file.name, nrows=0, encoding='utf-8-sig')
667
- except:
668
- df = pd.read_csv(file.name, nrows=0, encoding='cp1256')
669
- columns = list(df.columns)
670
- return gr.update(choices=columns, value=columns[0] if columns else None)
671
-
672
- def process_single(provider, api_key, model, text):
673
- if not text.strip():
674
- return "", "❌ متن خالی"
675
- if not api_key:
676
- return "", "❌ کلید API وارد نشده"
677
-
678
- try:
679
- prov = APIProvider(provider)
680
- anonymizer = MultiProviderAnonymizer(provider=prov, api_key=api_key, model=model)
681
- result = anonymizer.anonymize_text(text)
682
-
683
- if result["success"]:
684
- stats = result.get("statistics", {})
685
- return result["anonymized_text"], f"✅ C:{stats.get('company',0)} | P:{stats.get('person',0)} | A:{stats.get('amount',0)} | %:{stats.get('percent',0)}"
686
- else:
687
- return "", f"❌ {result.get('error', '')}"
688
- except Exception as e:
689
- return "", f"❌ {str(e)}"
690
-
691
- def start_batch(file, text_col, output_col, delay, rpm, retries, provider, api_key, model):
692
- if file is None:
693
- yield (0, "❌ فایل انتخاب نشده", "", "", None, gr.update(visible=False))
694
- return
695
- if not api_key:
696
- yield (0, "❌ کلید API وارد نشده", "", "", None, gr.update(visible=False))
697
- return
698
-
699
- prov = APIProvider(provider)
700
- rate_config = RateLimitConfig(
701
- requests_per_minute=int(rpm),
702
- min_delay_between_requests=float(delay),
703
- max_retries=int(retries)
704
- )
705
-
706
- processor = BatchProcessor(provider=prov, api_key=api_key, model=model, rate_limit_config=rate_config)
707
- batch_processor["instance"] = processor
708
-
709
- log_lines = []
710
- preview_data = []
711
-
712
- for update in processor.process_csv(file.name, text_col, output_col):
713
- update_type = update.get("type")
714
-
715
- if update_type == "error":
716
- log_lines.append(f"❌ {update['message']}")
717
- yield (0, f"❌ {update['message']}", "", "\n".join(log_lines), None, gr.update(visible=False))
718
- return
719
-
720
- elif update_type == "info":
721
- log_lines.append(f"ℹ️ {update['message']}")
722
-
723
- elif update_type == "progress":
724
- progress = update["progress"]
725
- current = update["current"]
726
- total = update["total"]
727
- elapsed = update["elapsed"]
728
- remaining = update["estimated_remaining"]
729
-
730
- progress_md = f"**{current}/{total}** ({progress:.1f}%) | ✅ {update['processed']} | ❌ {update['failed']}"
731
- time_md = f"⏱️ {elapsed/60:.1f}m | باقیمانده: {remaining/60:.1f}m"
732
-
733
- if current % 10 == 0:
734
- log_lines.append(f"📊 {current}/{total}")
735
-
736
- last_result = update.get("last_result", {})
737
- if last_result.get("success"):
738
- preview_data.append(["...", last_result.get("anonymized_text", "")[:80] + "...", "✅"])
739
- if len(preview_data) > 5:
740
- preview_data = preview_data[-5:]
741
-
742
- yield (progress, progress_md, time_md, "\n".join(log_lines[-15:]), preview_data if preview_data else None, gr.update(visible=False))
743
-
744
- elif update_type == "cancelled":
745
- log_lines.append("⏹️ لغو شد")
746
- yield (0, "⏹️ لغو شد", "", "\n".join(log_lines), preview_data if preview_data else None, gr.update(visible=False))
747
- return
748
-
749
- elif update_type == "complete":
750
- log_lines.append(f"✅ {update['message']}")
751
- progress_md = f"✅ تکمیل! {update['processed']}/{update['total']} موفق | {update['total_time']/60:.1f} دقیقه"
752
- yield (100, progress_md, "", "\n".join(log_lines), preview_data if preview_data else None, gr.update(value=update['output_path'], visible=True))
753
-
754
- def cancel():
755
- if batch_processor["instance"]:
756
- batch_processor["instance"].cancel()
757
- return "⏹️ لغو شد..."
758
-
759
- # اتصالات
760
- single_provider.change(update_models, [single_provider], [single_model])
761
- batch_provider.change(update_models, [batch_provider], [batch_model])
762
- csv_file.change(update_columns, [csv_file], [text_column])
763
-
764
- single_btn.click(process_single, [single_provider, single_api_key, single_model, single_input], [single_output, single_stats])
765
-
766
- start_btn.click(
767
- start_batch,
768
- [csv_file, text_column, output_column, delay_slider, rpm_slider, retries_slider, batch_provider, batch_api_key, batch_model],
769
- [progress_bar, progress_text, time_stats, process_log, preview_table, output_file]
770
- )
771
-
772
- cancel_btn.click(cancel, outputs=[process_log])
773
-
774
- return interface
775
-
776
-
777
- if __name__ == "__main__":
778
- interface = create_interface()
779
- interface.launch(server_name="0.0.0.0", server_port=7860, share=True, show_error=True)