Spaces:
Sleeping
Sleeping
| import requests | |
| import json | |
| import gradio as gr | |
| from typing import Dict, Any, List, Generator | |
| import os | |
| from dataclasses import dataclass | |
| import re | |
| import pandas as pd | |
| import time | |
| from datetime import datetime | |
| import threading | |
| from queue import Queue | |
| import io | |
| class CerebrasConfig: | |
| """تنظیمات Cerebras API""" | |
| api_key: str | |
| base_url: str = "https://api.cerebras.ai/v1" | |
| model: str = "llama-3.3-70b" | |
| max_tokens: int = 2000 | |
| temperature: float = 0.1 | |
| class RateLimitConfig: | |
| """تنظیمات محدودیت نرخ درخواست برای Cerebras""" | |
| # محدودیتهای Cerebras Free Tier | |
| requests_per_minute: int = 30 # حداکثر درخواست در دقیقه | |
| tokens_per_minute: int = 60000 # حداکثر توکن در دقیقه | |
| min_delay_between_requests: float = 2.0 # حداقل تأخیر بین درخواستها (ثانیه) | |
| max_retries: int = 5 # حداکثر تلاش مجدد | |
| initial_backoff: float = 5.0 # تأخیر اولیه برای backoff (ثانیه) | |
| max_backoff: float = 120.0 # حداکثر تأخیر backoff (ثانیه) | |
| backoff_multiplier: float = 2.0 # ضریب افزایش تأخیر | |
| class RateLimiter: | |
| """مدیریت محدودیت نرخ درخواست""" | |
| def __init__(self, config: RateLimitConfig): | |
| self.config = config | |
| self.request_times: List[float] = [] | |
| self.lock = threading.Lock() | |
| self.consecutive_failures = 0 | |
| def wait_if_needed(self) -> float: | |
| """انتظار تا زمان مجاز ارسال درخواست بعدی""" | |
| with self.lock: | |
| now = time.time() | |
| # پاک کردن درخواستهای قدیمیتر از 1 دقیقه | |
| self.request_times = [t for t in self.request_times if now - t < 60] | |
| # محاسبه زمان انتظار | |
| wait_time = 0.0 | |
| # اگر به محدودیت درخواست در دقیقه رسیدهایم | |
| if len(self.request_times) >= self.config.requests_per_minute: | |
| oldest_request = min(self.request_times) | |
| wait_time = max(wait_time, 60 - (now - oldest_request) + 1) | |
| # حداقل تأخیر بین درخواستها | |
| if self.request_times: | |
| time_since_last = now - max(self.request_times) | |
| if time_since_last < self.config.min_delay_between_requests: | |
| wait_time = max(wait_time, self.config.min_delay_between_requests - time_since_last) | |
| # افزایش تأخیر در صورت خطاهای متوالی | |
| if self.consecutive_failures > 0: | |
| failure_wait = min( | |
| self.config.initial_backoff * (self.config.backoff_multiplier ** self.consecutive_failures), | |
| self.config.max_backoff | |
| ) | |
| wait_time = max(wait_time, failure_wait) | |
| if wait_time > 0: | |
| time.sleep(wait_time) | |
| self.request_times.append(time.time()) | |
| return wait_time | |
| def report_success(self): | |
| """گزارش موفقیت درخواست""" | |
| with self.lock: | |
| self.consecutive_failures = 0 | |
| def report_failure(self, is_rate_limit: bool = False): | |
| """گزارش شکست درخواست""" | |
| with self.lock: | |
| if is_rate_limit: | |
| self.consecutive_failures += 1 | |
| else: | |
| # برای خطاهای غیر rate limit، کمتر افزایش میدهیم | |
| self.consecutive_failures = min(self.consecutive_failures + 0.5, 3) | |
| def get_estimated_wait_time(self) -> float: | |
| """تخمین زمان انتظار برای درخواست بعدی""" | |
| with self.lock: | |
| now = time.time() | |
| self.request_times = [t for t in self.request_times if now - t < 60] | |
| if len(self.request_times) >= self.config.requests_per_minute: | |
| oldest_request = min(self.request_times) | |
| return max(0, 60 - (now - oldest_request) + 1) | |
| return self.config.min_delay_between_requests | |
| class AdvancedCerebrasAnonymizer: | |
| """سیستم پیشرفته ناشناسسازی متون مالی/خبری فارسی""" | |
| def __init__(self, api_key: str = None, rate_limit_config: RateLimitConfig = None): | |
| if api_key is None: | |
| api_key = os.getenv("CEREBRAS_API_KEY") | |
| if not api_key: | |
| raise ValueError("کلید API یافت نشد") | |
| self.config = CerebrasConfig(api_key=api_key) | |
| self.rate_limit_config = rate_limit_config or RateLimitConfig() | |
| self.rate_limiter = RateLimiter(self.rate_limit_config) | |
| self.system_prompt = self._create_advanced_system_prompt() | |
| def _create_advanced_system_prompt(self) -> str: | |
| """ایجاد دستورالعمل سیستمی پیشرفته برای Cerebras""" | |
| return """شما یک «ناشناسساز متون مالی/خبری فارسی» هستید. وظیفهتان جایگزینی اسامی خاص و مقادیر عددی با شناسههای بیمعناست. | |
| ## **قوانین اندیسگذاری - CRITICAL** | |
| ### **1. ترتیب شمارهگذاری الزامی:** | |
| - شرکتها: company-01, company-02, company-03, company-04, ... (پیوسته و بدون گپ) | |
| - اشخاص: person-01, person-02, person-03, ... (پیوسته و بدون گپ) | |
| - اعداد: amount-01, amount-02, amount-03, ... (پیوسته و بدون گپ) | |
| - درصدها: percent-01, percent-02, percent-03, ... (پیوسته و بدون گپ) | |
| ### **2. ثبات شناسهها در متن:** | |
| - اگر "همراه اول" اولبار company-01 شد، در تمام متن همان باشد | |
| - اگر "مهدی احمدی" اولبار person-01 شد، در تمام متن همان باشد | |
| ### **3. تشخیص صحیح انواع:** | |
| **شرکت/سازمان:** همراه اول، بانک ملی، ایرانخودرو، سایپا، بانک مرکزی، سامانه کدال، وزارت نفت، سازمان تنظیم مقررات رادیویی، سازمان تامین اجتماعی | |
| **⚠️ CRITICAL - گروهها:** "گروه همراه اول"، "گروه اقتصادی آزادگان"، "گروه مالی صبا" → همه company-XX هستند (نه group-XX) | |
| **⚠️ CRITICAL - کلمات عمومی:** "سه شرکت دارویی"، "چند بانک"، "یک شرکت" → کلمات عمومی هستند، موجودیت نیستند (حفظ شوند) | |
| **⚠️ CRITICAL - نامهای مستعار:** "فاما" همان "فولاد مبارکه اصفهان" است → هر دو company-01 | |
| **شخص:** مهدی اخوان بهابادی، محمدرضا فرزین، ابوالفضل نجارزاده | |
| **عدد:** 37، 70، 677، 73.7، 178 (هر عددی) | |
| **درصد:** 37 درصدی، 15 درصدی، 53 درصد، 43% | |
| ## **مثالهای صحیح:** | |
| ### **مثال 1 (الگوی کامل):** | |
| **ورودی:** مهدی اخوان بهابادی، مدیرعامل همراه اول، اعلام کرد درآمد عملیاتی شرکت با رشد 37 درصدی به 70 هزار و 677 میلیارد تومان رسیده است. | |
| **خروجی صحیح:** person-01، مدیرعامل company-01، اعلام کرد درآمد عملیاتی شرکت با رشد percent-01 به amount-01 رسیده است. | |
| ### **مثال 2:** | |
| **ورودی:** بانک مرکزی و بانک ملی با همکاری محمدرضا فرزین، 60 درصد سپردهها را مدیریت کردند. | |
| **خروجی:** company-01 و company-02 با همکاری person-01، percent-01 سپردهها را مدیریت کردند. | |
| ## **⚠️ CRITICAL - دورههای زمانی را حفظ کن:** | |
| - "۹ ماهه" → حفظ شود (نه amount-XX) | |
| - "۵ ماهه سال" → حفظ شود (نه amount-XX) | |
| - "۳ ماهه اول" → حفظ شود (نه amount-XX) | |
| ## **موارد حفظ شده:** | |
| - تاریخها: 1404/04/23، 30 آذر 1403، پاییز 1401 | |
| - فصلهای سال: پاییز، بهار، تابستان، زمستان | |
| - عناوین شغلی: مدیرعامل، رئیس کل، مدیرکل | |
| - واحدها: میلیارد تومان، همت، ریال، ماه، سال | |
| - مکانها: تهران، اصفهان، ایران | |
| **فقط متن ناشناسشده را برگردان - هیچ توضیح اضافی نیاز نیست.** | |
| """ | |
| def _make_api_request_with_retry(self, text: str) -> Dict[str, Any]: | |
| """ارسال درخواست به Cerebras API با مدیریت rate limit و retry""" | |
| headers = { | |
| "Authorization": f"Bearer {self.config.api_key}", | |
| "Content-Type": "application/json" | |
| } | |
| payload = { | |
| "messages": [ | |
| {"role": "system", "content": self.system_prompt}, | |
| {"role": "user", "content": text} | |
| ], | |
| "model": self.config.model, | |
| "temperature": self.config.temperature, | |
| "max_tokens": self.config.max_tokens | |
| } | |
| last_error = None | |
| for attempt in range(self.rate_limit_config.max_retries): | |
| # انتظار قبل از ارسال درخواست | |
| wait_time = self.rate_limiter.wait_if_needed() | |
| try: | |
| response = requests.post( | |
| f"{self.config.base_url}/chat/completions", | |
| headers=headers, | |
| json=payload, | |
| timeout=60 | |
| ) | |
| # بررسی خطای rate limit (429) | |
| if response.status_code == 429: | |
| self.rate_limiter.report_failure(is_rate_limit=True) | |
| # استخراج زمان انتظار از هدر (اگر موجود باشد) | |
| retry_after = response.headers.get('Retry-After') | |
| if retry_after: | |
| wait_seconds = int(retry_after) | |
| else: | |
| # محاسبه exponential backoff | |
| wait_seconds = min( | |
| self.rate_limit_config.initial_backoff * (self.rate_limit_config.backoff_multiplier ** attempt), | |
| self.rate_limit_config.max_backoff | |
| ) | |
| last_error = f"محدودیت نرخ درخواست (429). تلاش {attempt + 1}/{self.rate_limit_config.max_retries}. انتظار {wait_seconds:.1f} ثانیه..." | |
| time.sleep(wait_seconds) | |
| continue | |
| response.raise_for_status() | |
| self.rate_limiter.report_success() | |
| return response.json() | |
| except requests.exceptions.Timeout: | |
| self.rate_limiter.report_failure(is_rate_limit=False) | |
| last_error = f"خطای timeout. تلاش {attempt + 1}/{self.rate_limit_config.max_retries}" | |
| time.sleep(self.rate_limit_config.initial_backoff) | |
| except requests.exceptions.RequestException as e: | |
| self.rate_limiter.report_failure(is_rate_limit=False) | |
| last_error = f"خطای شبکه: {str(e)}. تلاش {attempt + 1}/{self.rate_limit_config.max_retries}" | |
| time.sleep(self.rate_limit_config.initial_backoff) | |
| raise Exception(f"ناموفق پس از {self.rate_limit_config.max_retries} تلاش. آخرین خطا: {last_error}") | |
| def anonymize_text(self, text: str) -> Dict[str, Any]: | |
| """ناشناسسازی متن با استفاده از Cerebras""" | |
| if not text or not text.strip(): | |
| return { | |
| "success": False, | |
| "error": "متن ورودی خالی است", | |
| "anonymized_text": "" | |
| } | |
| try: | |
| response = self._make_api_request_with_retry(text) | |
| if "choices" not in response or not response["choices"]: | |
| return { | |
| "success": False, | |
| "error": "پاسخ نامعتبر از API", | |
| "anonymized_text": "" | |
| } | |
| content = response["choices"][0]["message"]["content"] | |
| content = self._clean_markdown(content) | |
| content = content.strip() | |
| analysis = self._analyze_anonymized_text(content) | |
| return { | |
| "success": True, | |
| "anonymized_text": content, | |
| "entities": analysis["entities"], | |
| "statistics": analysis["statistics"], | |
| "usage": response.get("usage", {}) | |
| } | |
| except Exception as e: | |
| return { | |
| "success": False, | |
| "error": f"خطا در پردازش: {str(e)}", | |
| "anonymized_text": "" | |
| } | |
| def _clean_markdown(self, content: str) -> str: | |
| """پاک کردن markdown از پاسخ""" | |
| if "```" in content: | |
| lines = content.split('\n') | |
| clean_lines = [] | |
| skip = False | |
| for line in lines: | |
| if line.strip().startswith('```'): | |
| skip = not skip | |
| continue | |
| if not skip: | |
| clean_lines.append(line) | |
| content = '\n'.join(clean_lines) | |
| return content | |
| def _analyze_anonymized_text(self, text: str) -> Dict[str, Any]: | |
| """تحلیل متن ناشناسسازی شده""" | |
| companies = re.findall(r'company-(\d+)', text) | |
| persons = re.findall(r'person-(\d+)', text) | |
| amounts = re.findall(r'amount-(\d+)', text) | |
| percents = re.findall(r'percent-(\d+)', text) | |
| statistics = { | |
| "company": len(set(companies)), | |
| "person": len(set(persons)), | |
| "amount": len(set(amounts)), | |
| "percent": len(set(percents)), | |
| "total_replacements": len(companies) + len(persons) + len(amounts) + len(percents) | |
| } | |
| entities = { | |
| "companies": sorted(list(set(companies)), key=lambda x: int(x)), | |
| "persons": sorted(list(set(persons)), key=lambda x: int(x)), | |
| "amounts": sorted(list(set(amounts)), key=lambda x: int(x)), | |
| "percents": sorted(list(set(percents)), key=lambda x: int(x)) | |
| } | |
| return { | |
| "statistics": statistics, | |
| "entities": entities | |
| } | |
| class BatchProcessor: | |
| """پردازشگر دستهای فایلهای CSV""" | |
| def __init__(self, api_key: str, rate_limit_config: RateLimitConfig = None): | |
| self.api_key = api_key | |
| self.rate_limit_config = rate_limit_config or RateLimitConfig() | |
| self.anonymizer = None | |
| self.is_cancelled = False | |
| self.current_progress = 0 | |
| self.total_rows = 0 | |
| self.processed_rows = 0 | |
| self.failed_rows = 0 | |
| self.start_time = None | |
| def cancel(self): | |
| """لغو پردازش""" | |
| self.is_cancelled = True | |
| def reset(self): | |
| """بازنشانی وضعیت""" | |
| self.is_cancelled = False | |
| self.current_progress = 0 | |
| self.total_rows = 0 | |
| self.processed_rows = 0 | |
| self.failed_rows = 0 | |
| self.start_time = None | |
| def process_csv( | |
| self, | |
| file_path: str, | |
| text_column: str, | |
| output_column: str = "anonymized_text", | |
| progress_callback=None | |
| ) -> Generator[Dict[str, Any], None, None]: | |
| """پردازش فایل CSV به صورت streaming""" | |
| self.reset() | |
| self.start_time = time.time() | |
| # خواندن فایل CSV | |
| try: | |
| df = pd.read_csv(file_path, encoding='utf-8') | |
| except UnicodeDecodeError: | |
| try: | |
| df = pd.read_csv(file_path, encoding='utf-8-sig') | |
| except: | |
| df = pd.read_csv(file_path, encoding='cp1256') | |
| if text_column not in df.columns: | |
| yield { | |
| "type": "error", | |
| "message": f"ستون '{text_column}' در فایل یافت نشد. ستونهای موجود: {list(df.columns)}" | |
| } | |
| return | |
| self.total_rows = len(df) | |
| # ایجاد anonymizer | |
| self.anonymizer = AdvancedCerebrasAnonymizer( | |
| api_key=self.api_key, | |
| rate_limit_config=self.rate_limit_config | |
| ) | |
| # ایجاد ستون خروجی | |
| df[output_column] = "" | |
| df["anonymization_status"] = "" | |
| df["entities_found"] = "" | |
| yield { | |
| "type": "info", | |
| "message": f"🚀 شروع پردازش {self.total_rows} ردیف...", | |
| "total": self.total_rows | |
| } | |
| results = [] | |
| for idx, row in df.iterrows(): | |
| if self.is_cancelled: | |
| yield { | |
| "type": "cancelled", | |
| "message": "پردازش توسط کاربر لغو شد", | |
| "processed": self.processed_rows, | |
| "failed": self.failed_rows | |
| } | |
| break | |
| text = str(row[text_column]) if pd.notna(row[text_column]) else "" | |
| if not text.strip(): | |
| df.at[idx, output_column] = "" | |
| df.at[idx, "anonymization_status"] = "خالی" | |
| df.at[idx, "entities_found"] = "" | |
| self.processed_rows += 1 | |
| continue | |
| # پردازش متن | |
| result = self.anonymizer.anonymize_text(text) | |
| if result["success"]: | |
| df.at[idx, output_column] = result["anonymized_text"] | |
| df.at[idx, "anonymization_status"] = "موفق" | |
| stats = result.get("statistics", {}) | |
| entities_summary = f"شرکت:{stats.get('company',0)} | شخص:{stats.get('person',0)} | مبلغ:{stats.get('amount',0)} | درصد:{stats.get('percent',0)}" | |
| df.at[idx, "entities_found"] = entities_summary | |
| self.processed_rows += 1 | |
| else: | |
| df.at[idx, output_column] = f"خطا: {result.get('error', 'نامشخص')}" | |
| df.at[idx, "anonymization_status"] = "ناموفق" | |
| df.at[idx, "entities_found"] = "" | |
| self.failed_rows += 1 | |
| # محاسبه پیشرفت و زمان باقیمانده | |
| self.current_progress = (idx + 1) / self.total_rows * 100 | |
| elapsed = time.time() - self.start_time | |
| avg_time_per_row = elapsed / (idx + 1) | |
| remaining_rows = self.total_rows - (idx + 1) | |
| estimated_remaining = avg_time_per_row * remaining_rows | |
| # تخمین زمان انتظار بعدی | |
| next_wait = self.anonymizer.rate_limiter.get_estimated_wait_time() | |
| yield { | |
| "type": "progress", | |
| "current": idx + 1, | |
| "total": self.total_rows, | |
| "progress": self.current_progress, | |
| "processed": self.processed_rows, | |
| "failed": self.failed_rows, | |
| "elapsed": elapsed, | |
| "estimated_remaining": estimated_remaining, | |
| "next_wait": next_wait, | |
| "last_result": result | |
| } | |
| # ذخیره نتیجه نهایی | |
| if not self.is_cancelled: | |
| output_path = file_path.replace('.csv', '_anonymized.csv') | |
| if output_path == file_path: | |
| output_path = file_path + '_anonymized.csv' | |
| df.to_csv(output_path, index=False, encoding='utf-8-sig') | |
| total_time = time.time() - self.start_time | |
| yield { | |
| "type": "complete", | |
| "message": "✅ پردازش با موفقیت تکمیل شد!", | |
| "output_path": output_path, | |
| "total": self.total_rows, | |
| "processed": self.processed_rows, | |
| "failed": self.failed_rows, | |
| "total_time": total_time, | |
| "dataframe": df | |
| } | |
| def create_batch_interface(): | |
| """ایجاد رابط کاربری برای پردازش دستهای""" | |
| api_key_available = bool(os.getenv("CEREBRAS_API_KEY")) | |
| custom_css = """ | |
| .gradio-container { | |
| font-family: 'Tahoma', 'Arial', sans-serif !important; | |
| direction: rtl; | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| } | |
| .progress-bar { | |
| background-color: #e9ecef; | |
| border-radius: 10px; | |
| height: 30px; | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| background: linear-gradient(90deg, #28a745, #20c997); | |
| height: 100%; | |
| transition: width 0.3s ease; | |
| } | |
| .stats-card { | |
| background-color: #f8f9fa; | |
| border-radius: 10px; | |
| padding: 15px; | |
| margin: 10px 0; | |
| border: 1px solid #dee2e6; | |
| } | |
| .warning-box { | |
| background-color: #fff3cd; | |
| border: 2px solid #ffeaa7; | |
| border-radius: 12px; | |
| padding: 15px; | |
| color: #856404; | |
| margin: 10px 0; | |
| } | |
| .success-box { | |
| background-color: #d4edda; | |
| border: 2px solid #c3e6cb; | |
| border-radius: 12px; | |
| padding: 15px; | |
| color: #155724; | |
| margin: 10px 0; | |
| } | |
| .info-box { | |
| background-color: #d1ecf1; | |
| border: 2px solid #bee5eb; | |
| border-radius: 12px; | |
| padding: 15px; | |
| color: #0c5460; | |
| margin: 10px 0; | |
| } | |
| """ | |
| # متغیرهای سراسری برای مدیریت پردازش | |
| batch_processor = {"instance": None} | |
| with gr.Blocks(css=custom_css, title="پردازش دستهای ناشناسسازی با Cerebras", theme=gr.themes.Soft()) as interface: | |
| gr.Markdown(""" | |
| # 🔒 سیستم پردازش دستهای ناشناسسازی متون فارسی | |
| ### ⚡ قدرتگرفته از Cerebras AI با مدیریت هوشمند Rate Limit | |
| """) | |
| with gr.Tabs(): | |
| # تب پردازش تکی | |
| with gr.Tab("📝 پردازش تکی"): | |
| if api_key_available: | |
| gr.Markdown('<div class="success-box">✅ <strong>سیستم آماده است</strong> - کلید API تنظیم شده</div>') | |
| single_api_key = gr.Textbox(visible=False, value="") | |
| else: | |
| gr.Markdown('<div class="warning-box">⚠️ <strong>کلید API تنظیم نشده</strong> - لطفاً کلید خود را وارد کنید</div>') | |
| single_api_key = gr.Textbox(label="🔑 کلید Cerebras API", placeholder="csk-...", type="password") | |
| with gr.Row(): | |
| with gr.Column(): | |
| single_input = gr.Textbox(label="📝 متن ورودی", placeholder="متن خود را وارد کنید...", lines=10) | |
| single_btn = gr.Button("🔒 ناشناسسازی", variant="primary") | |
| with gr.Column(): | |
| single_output = gr.Textbox(label="🎯 متن ناشناسسازی شده", lines=10) | |
| single_stats = gr.Markdown() | |
| # تب پردازش دستهای | |
| with gr.Tab("📁 پردازش دستهای CSV"): | |
| gr.Markdown(""" | |
| <div class="info-box"> | |
| 📌 <strong>راهنمای پردازش دستهای:</strong><br> | |
| 1. فایل CSV خود را آپلود کنید<br> | |
| 2. ستون حاوی متن را انتخاب کنید<br> | |
| 3. تنظیمات Rate Limit را بررسی کنید<br> | |
| 4. پردازش را شروع کنید<br><br> | |
| ⚠️ <strong>نکته مهم:</strong> برای جلوگیری از خطای 429، تأخیر بین درخواستها به صورت خودکار مدیریت میشود. | |
| </div> | |
| """) | |
| if not api_key_available: | |
| batch_api_key = gr.Textbox(label="🔑 کلید Cerebras API", placeholder="csk-...", type="password") | |
| else: | |
| batch_api_key = gr.Textbox(visible=False, value="") | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| csv_file = gr.File(label="📂 فایل CSV", file_types=[".csv"]) | |
| with gr.Row(): | |
| text_column = gr.Dropdown( | |
| label="📑 ستون متن", | |
| choices=[], | |
| interactive=True, | |
| info="ستون حاوی متن برای ناشناسسازی" | |
| ) | |
| output_column = gr.Textbox( | |
| label="📤 نام ستون خروجی", | |
| value="anonymized_text", | |
| info="نام ستون برای ذخیره نتایج" | |
| ) | |
| with gr.Column(scale=1): | |
| gr.Markdown("### ⚙️ تنظیمات Rate Limit") | |
| delay_between_requests = gr.Slider( | |
| label="⏱️ حداقل تأخیر بین درخواستها (ثانیه)", | |
| minimum=1.0, | |
| maximum=10.0, | |
| value=2.5, | |
| step=0.5, | |
| info="افزایش این مقدار از خطای 429 جلوگیری میکند" | |
| ) | |
| requests_per_minute = gr.Slider( | |
| label="📊 حداکثر درخواست در دقیقه", | |
| minimum=5, | |
| maximum=30, | |
| value=20, | |
| step=1, | |
| info="محدودیت Cerebras Free: 30 درخواست/دقیقه" | |
| ) | |
| max_retries = gr.Slider( | |
| label="🔄 حداکثر تلاش مجدد", | |
| minimum=1, | |
| maximum=10, | |
| value=5, | |
| step=1, | |
| info="تعداد تلاش در صورت خطای 429" | |
| ) | |
| with gr.Row(): | |
| start_btn = gr.Button("🚀 شروع پردازش", variant="primary", size="lg") | |
| cancel_btn = gr.Button("⏹️ لغو پردازش", variant="stop", size="lg") | |
| # نمایش پیشرفت | |
| progress_bar = gr.Slider( | |
| label="📊 پیشرفت کلی", | |
| minimum=0, | |
| maximum=100, | |
| value=0, | |
| interactive=False | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| progress_text = gr.Markdown("### 📈 وضعیت پردازش\nدر انتظار شروع...") | |
| with gr.Column(): | |
| time_stats = gr.Markdown("### ⏱️ زمانبندی\nدر انتظار شروع...") | |
| # لاگ پردازش | |
| process_log = gr.Textbox( | |
| label="📋 لاگ پردازش", | |
| lines=8, | |
| max_lines=15, | |
| interactive=False | |
| ) | |
| # نمایش نمونه نتایج | |
| with gr.Accordion("👁️ پیشنمایش نتایج", open=False): | |
| preview_table = gr.Dataframe( | |
| label="نمونه نتایج", | |
| headers=["متن اصلی", "متن ناشناسشده", "وضعیت"], | |
| interactive=False | |
| ) | |
| # دانلود نتیجه | |
| output_file = gr.File(label="📥 دانلود فایل خروجی", visible=False) | |
| # تب تنظیمات | |
| with gr.Tab("⚙️ تنظیمات و راهنما"): | |
| gr.Markdown(""" | |
| ## 📖 راهنمای محدودیتهای Cerebras API | |
| ### 🔒 محدودیتهای Free Tier: | |
| | پارامتر | مقدار | | |
| |---------|--------| | |
| | درخواست در دقیقه | 30 | | |
| | توکن در دقیقه | 60,000 | | |
| | توکن در روز | 1,000,000 | | |
| ### ⚡ نکات بهینهسازی: | |
| 1. **تأخیر بین درخواستها:** حداقل 2 ثانیه بین هر درخواست | |
| 2. **Exponential Backoff:** در صورت خطای 429، تأخیر به صورت نمایی افزایش مییابد | |
| 3. **Retry Logic:** سیستم تا 5 بار تلاش مجدد میکند | |
| ### 🎯 پیشنهادات: | |
| - برای فایلهای بزرگ (>100 ردیف)، تأخیر را روی 3 ثانیه تنظیم کنید | |
| - اگر خطای 429 زیاد دیدید، تأخیر را افزایش دهید | |
| - در ساعات شلوغ، محدودیتها ممکن است سختتر شوند | |
| ### 📊 فرمت فایل CSV: | |
| - **Encoding:** UTF-8 یا UTF-8-BOM پیشنهاد میشود | |
| - **ستونها:** حداقل یک ستون حاوی متن فارسی | |
| - **حجم:** بدون محدودیت (اما پردازش فایلهای بزرگ زمانبر است) | |
| """) | |
| # توابع | |
| def update_columns(file): | |
| """بروزرسانی لیست ستونها بعد از آپلود فایل""" | |
| if file is None: | |
| return gr.update(choices=[], value=None) | |
| try: | |
| df = pd.read_csv(file.name, encoding='utf-8', nrows=5) | |
| except: | |
| try: | |
| df = pd.read_csv(file.name, encoding='utf-8-sig', nrows=5) | |
| except: | |
| df = pd.read_csv(file.name, encoding='cp1256', nrows=5) | |
| columns = list(df.columns) | |
| return gr.update(choices=columns, value=columns[0] if columns else None) | |
| def process_single_text(text, api_key): | |
| """پردازش متن تکی""" | |
| if not text.strip(): | |
| return "", "⚠️ متن ورودی خالی است" | |
| key = api_key if api_key else os.getenv("CEREBRAS_API_KEY") | |
| if not key: | |
| return "", "❌ کلید API وارد نشده است" | |
| try: | |
| anonymizer = AdvancedCerebrasAnonymizer(api_key=key) | |
| result = anonymizer.anonymize_text(text) | |
| if result["success"]: | |
| stats = result.get("statistics", {}) | |
| stats_md = f""" | |
| ### ✅ پردازش موفق | |
| - 🏢 شرکتها: {stats.get('company', 0)} | |
| - 👤 اشخاص: {stats.get('person', 0)} | |
| - 💰 مبالغ: {stats.get('amount', 0)} | |
| - 📊 درصدها: {stats.get('percent', 0)} | |
| """ | |
| return result["anonymized_text"], stats_md | |
| else: | |
| return "", f"❌ خطا: {result.get('error', 'نامشخص')}" | |
| except Exception as e: | |
| return "", f"❌ خطا: {str(e)}" | |
| def start_batch_processing( | |
| file, | |
| text_col, | |
| output_col, | |
| delay, | |
| rpm, | |
| retries, | |
| api_key | |
| ): | |
| """شروع پردازش دستهای""" | |
| if file is None: | |
| yield ( | |
| 0, | |
| "### ❌ خطا\nفایل انتخاب نشده است", | |
| "", | |
| "", | |
| None, | |
| gr.update(visible=False) | |
| ) | |
| return | |
| key = api_key if api_key else os.getenv("CEREBRAS_API_KEY") | |
| if not key: | |
| yield ( | |
| 0, | |
| "### ❌ خطا\nکلید API وارد نشده است", | |
| "", | |
| "", | |
| None, | |
| gr.update(visible=False) | |
| ) | |
| return | |
| # تنظیم rate limit | |
| rate_config = RateLimitConfig( | |
| requests_per_minute=int(rpm), | |
| min_delay_between_requests=float(delay), | |
| max_retries=int(retries) | |
| ) | |
| # ایجاد پردازشگر | |
| processor = BatchProcessor(api_key=key, rate_limit_config=rate_config) | |
| batch_processor["instance"] = processor | |
| log_lines = [] | |
| preview_data = [] | |
| # پردازش | |
| for update in processor.process_csv(file.name, text_col, output_col): | |
| update_type = update.get("type") | |
| if update_type == "error": | |
| log_lines.append(f"❌ {update['message']}") | |
| yield ( | |
| 0, | |
| f"### ❌ خطا\n{update['message']}", | |
| "", | |
| "\n".join(log_lines), | |
| None, | |
| gr.update(visible=False) | |
| ) | |
| return | |
| elif update_type == "info": | |
| log_lines.append(f"ℹ️ {update['message']}") | |
| elif update_type == "progress": | |
| progress = update["progress"] | |
| current = update["current"] | |
| total = update["total"] | |
| processed = update["processed"] | |
| failed = update["failed"] | |
| elapsed = update["elapsed"] | |
| remaining = update["estimated_remaining"] | |
| next_wait = update.get("next_wait", 0) | |
| progress_md = f""" | |
| ### 📈 وضعیت پردازش | |
| - **پردازش شده:** {current}/{total} ({progress:.1f}%) | |
| - **موفق:** {processed} ✅ | |
| - **ناموفق:** {failed} ❌ | |
| - **تأخیر بعدی:** {next_wait:.1f} ثانیه | |
| """ | |
| time_md = f""" | |
| ### ⏱️ زمانبندی | |
| - **سپری شده:** {elapsed/60:.1f} دقیقه | |
| - **تخمین باقیمانده:** {remaining/60:.1f} دقیقه | |
| - **سرعت:** {current/elapsed*60:.1f} ردیف/دقیقه | |
| """ | |
| # بروزرسانی لاگ هر 10 ردیف | |
| if current % 10 == 0 or current == total: | |
| log_lines.append(f"📊 پردازش {current}/{total} - موفق: {processed}, ناموفق: {failed}") | |
| # بروزرسانی پیشنمایش | |
| last_result = update.get("last_result", {}) | |
| if last_result.get("success"): | |
| preview_data.append([ | |
| "...", # متن اصلی خلاصه | |
| last_result.get("anonymized_text", "")[:100] + "...", | |
| "✅ موفق" | |
| ]) | |
| if len(preview_data) > 5: | |
| preview_data = preview_data[-5:] | |
| yield ( | |
| progress, | |
| progress_md, | |
| time_md, | |
| "\n".join(log_lines[-20:]), # فقط 20 خط آخر | |
| preview_data if preview_data else None, | |
| gr.update(visible=False) | |
| ) | |
| elif update_type == "cancelled": | |
| log_lines.append(f"⏹️ {update['message']}") | |
| yield ( | |
| 0, | |
| f"### ⏹️ لغو شد\nپردازش شده: {update['processed']}, ناموفق: {update['failed']}", | |
| "", | |
| "\n".join(log_lines), | |
| preview_data if preview_data else None, | |
| gr.update(visible=False) | |
| ) | |
| return | |
| elif update_type == "complete": | |
| total_time = update["total_time"] | |
| log_lines.append(f"✅ {update['message']}") | |
| log_lines.append(f"📁 فایل خروجی: {update['output_path']}") | |
| progress_md = f""" | |
| ### ✅ پردازش تکمیل شد! | |
| - **کل ردیفها:** {update['total']} | |
| - **موفق:** {update['processed']} ✅ | |
| - **ناموفق:** {update['failed']} ❌ | |
| - **زمان کل:** {total_time/60:.1f} دقیقه | |
| """ | |
| time_md = f""" | |
| ### 📊 آمار نهایی | |
| - **سرعت میانگین:** {update['total']/total_time*60:.1f} ردیف/دقیقه | |
| - **نرخ موفقیت:** {update['processed']/update['total']*100:.1f}% | |
| """ | |
| yield ( | |
| 100, | |
| progress_md, | |
| time_md, | |
| "\n".join(log_lines), | |
| preview_data if preview_data else None, | |
| gr.update(value=update['output_path'], visible=True) | |
| ) | |
| def cancel_processing(): | |
| """لغو پردازش""" | |
| if batch_processor["instance"]: | |
| batch_processor["instance"].cancel() | |
| return "⏹️ درخواست لغو ارسال شد..." | |
| # اتصال رویدادها | |
| csv_file.change( | |
| fn=update_columns, | |
| inputs=[csv_file], | |
| outputs=[text_column] | |
| ) | |
| single_btn.click( | |
| fn=process_single_text, | |
| inputs=[single_input, single_api_key], | |
| outputs=[single_output, single_stats] | |
| ) | |
| start_btn.click( | |
| fn=start_batch_processing, | |
| inputs=[ | |
| csv_file, | |
| text_column, | |
| output_column, | |
| delay_between_requests, | |
| requests_per_minute, | |
| max_retries, | |
| batch_api_key | |
| ], | |
| outputs=[ | |
| progress_bar, | |
| progress_text, | |
| time_stats, | |
| process_log, | |
| preview_table, | |
| output_file | |
| ] | |
| ) | |
| cancel_btn.click( | |
| fn=cancel_processing, | |
| outputs=[process_log] | |
| ) | |
| return interface | |
| # اجرای برنامه | |
| if __name__ == "__main__": | |
| interface = create_batch_interface() | |
| interface.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=True, | |
| show_error=True | |
| ) | |