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 = 25 # حداکثر درخواست در دقیقه (کمتر از 30 برای امنیت) | |
| tokens_per_minute: int = 60000 # حداکثر توکن در دقیقه | |
| min_delay_between_requests: float = 3.0 # حداقل تأخیر بین درخواستها (ثانیه) - افزایش از 2 به 3 | |
| max_retries: int = 7 # حداکثر تلاش مجدد - افزایش از 5 به 7 | |
| initial_backoff: float = 10.0 # تأخیر اولیه برای backoff (ثانیه) - افزایش از 5 به 10 | |
| max_backoff: float = 180.0 # حداکثر تأخیر backoff (ثانیه) - افزایش از 120 به 180 | |
| 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, ... (پیوسته و بدون گپ) | |
| - اشخاص: person-01, person-02, person-03, ... (پیوسته و بدون گپ) | |
| - اعداد/مبالغ: amount-01, amount-02, amount-03, ... (پیوسته و بدون گپ) | |
| - درصدها: percent-01, percent-02, percent-03, ... (پیوسته و بدون گپ) | |
| ### **2. ثبات شناسهها در متن:** | |
| - اگر "همراه اول" اولبار company-01 شد، در تمام متن همان باشد | |
| - اگر "مهدی احمدی" اولبار person-01 شد، در تمام متن همان باشد | |
| ## **⚠️ قوانین حیاتی برای واحدها و مبالغ:** | |
| ### **قانون 1: مبالغ کامل را یکجا جایگزین کن (بدون واحد)** | |
| - "23 هزار و 296 میلیارد تومان" → `amount-01` ✅ | |
| - "23 هزار و 296 میلیارد تومان" → `amount-01 تومان` ❌ | |
| - "500 میلیون دلار" → `amount-01` ✅ | |
| - "6 میلیارد تومان" → `amount-01` ✅ | |
| - "681,667 میلیارد ریال" → `amount-01` ✅ | |
| ### **قانون 2: پسوندهای صفتی (-ی) را حفظ کن** | |
| - "155 هزار میلیارد ریالی" → `amount-01 ریالی` ✅ | |
| - "2700 میلیارد تومانی" → `amount-01 تومانی` ✅ | |
| - "37 درصدی" → `percent-01` ✅ (پسوند در درصد حذف میشود) | |
| ### **قانون 3: کلمه "درصد" را حذف کن** | |
| - "4.58 درصد" → `percent-01` ✅ | |
| - "4.58 درصد" → `percent-01 درصد` ❌ | |
| - "75 درصد" → `percent-01` ✅ | |
| - "منفی 345 درصد" → `منفی percent-01` ✅ | |
| ### **قانون 4: "درصدی" را به percent تبدیل کن** | |
| - "37 درصدی" → `percent-01` ✅ | |
| - "رشد 14 درصدی" → `رشد percent-01` ✅ | |
| ## **⚠️ موارد حفظ شده (CRITICAL):** | |
| ### **1. سامانه کدال - حفظ شود!** | |
| - "سامانه کدال" → `سامانه کدال` ✅ (تغییر نکند!) | |
| - "سامانه کدال" → `company-XX` ❌ (اشتباه!) | |
| ### **2. تاریخها و سالها** | |
| - "سال 1402" → `سال 1402` ✅ | |
| - "سال 1402" → `سال amount-01` ❌ | |
| - "1404/04/29" → `1404/04/29` ✅ | |
| - "30 آذر 1403" → `30 آذر 1403` ✅ | |
| - "پاییز ۱۴۰۱" → `پاییز ۱۴۰۱` ✅ | |
| ### **3. دورههای زمانی** | |
| - "۵ ماهه سال" → حفظ شود ✅ | |
| - "سهماهه نخست" → حفظ شود ✅ | |
| - "۹ ماهه" → حفظ شود ✅ | |
| ### **4. عناوین شغلی و نقشها** | |
| - "مدیرعامل"، "رئیس هیئتمدیره"، "بازرس قانونی" → حفظ شوند ✅ | |
| ### **5. کلمات عمومی بدون نام خاص** | |
| - "سه شرکت دارویی" → حفظ شود ✅ (company نشود) | |
| - "چند بانک" → حفظ شود ✅ | |
| - "12 بانک کشور" → حفظ شود ✅ | |
| - "سه خودروساز بزرگ" → حفظ شود ✅ | |
| ## **تشخیص صحیح انواع موجودیتها:** | |
| ### **شرکت/سازمان (company-XX):** | |
| - نامهای خاص شرکت: ایران خودرو، بانک ملی، همراه اول، پتروشیمی بوعلی سینا | |
| - سازمانهای دولتی: سازمان تامین اجتماعی، وزارت نفت، سازمان بورس | |
| - گروهها: "گروه همراه اول" → company-XX ✅ (نه group-XX) | |
| - بازرس/حسابرس: "شرکت وانیا نیک تدبیر" (بازرس) → company-XX ✅ | |
| ### **شخص (person-XX):** | |
| - نام و نامخانوادگی اشخاص حقیقی | |
| - مثال: مهدی اخوان بهابادی، فرجاله قدمی | |
| ### **مبلغ/عدد (amount-XX):** | |
| - مبالغ مالی: "23 هزار میلیارد تومان" → amount-01 | |
| - تعداد: "537 هزار و 736 دستگاه" → amount-01 | |
| - اعداد اعشاری: "73.7 میلیون نفر" → amount-01 | |
| - ⚠️ سالها amount نیستند! | |
| ### **درصد (percent-XX):** | |
| - "4.58 درصد" → percent-01 | |
| - "75 درصد" → percent-01 | |
| - "37 درصدی" → percent-01 | |
| - "منفی 345 درصد" → منفی percent-01 | |
| ## **مثالهای صحیح:** | |
| ### **مثال 1:** | |
| **ورودی:** ایران خودرو در اسفندماه سال 1402 حدود 23 هزار و 296 میلیارد تومان درآمد کسب کرد که در مقایسه با بهمن 4.58 درصد افزایش داشت. | |
| **خروجی:** company-01 در اسفندماه سال 1402 حدود amount-01 درآمد کسب کرد که در مقایسه با بهمن percent-01 افزایش داشت. | |
| ⚠️ توجه: "تومان" و "درصد" حذف شدند، "سال 1402" حفظ شد. | |
| ### **مثال 2:** | |
| **ورودی:** بانک پاسارگاد با شناسایی سود خالص 155 هزار میلیارد ریالی در رده دوم قرار گرفت. | |
| **خروجی:** company-01 با شناسایی سود خالص amount-01 ریالی در رده دوم قرار گرفت. | |
| ⚠️ توجه: "ریالی" (با پسوند صفتی) حفظ شد. | |
| ### **مثال 3:** | |
| **ورودی:** شرکت تیپیکو گزارش خود را در سامانه کدال منتشر کرد. | |
| **خروجی:** company-01 گزارش خود را در سامانه کدال منتشر کرد. | |
| ⚠️ توجه: "سامانه کدال" حفظ شد (company نشد). | |
| ### **مثال 4:** | |
| **ورودی:** رشد 14 درصدی سرمایهگذاریها به 5000 میلیارد تومان رسید. | |
| **خروجی:** رشد percent-01 سرمایهگذاریها به amount-01 رسید. | |
| ⚠️ توجه: "درصدی" و "تومان" حذف شدند. | |
| ### **مثال 5:** | |
| **ورودی:** زیان خالص 2700 میلیارد تومانی در سهماهه نخست 1404 گزارش کرد. | |
| **خروجی:** زیان خالص amount-01 تومانی در سهماهه نخست 1404 گزارش کرد. | |
| ⚠️ توجه: "تومانی" (پسوند صفتی) حفظ شد، "سهماهه نخست 1404" حفظ شد. | |
| ### **مثال 6:** | |
| **ورودی:** سازمان تامین اجتماعی دارای سه شرکت دارویی است. | |
| **خروجی:** company-01 دارای سه شرکت دارویی است. | |
| ⚠️ توجه: "سه شرکت دارویی" کلمه عمومی است و حفظ شد. | |
| ## **خلاصه قوانین:** | |
| 1. مبالغ کامل → amount-XX (بدون واحد) | |
| 2. پسوند صفتی (-ی) → حفظ شود (amount-01 ریالی، amount-01 تومانی) | |
| 3. درصد/درصدی → percent-XX (بدون کلمه درصد) | |
| 4. سامانه کدال → حفظ شود | |
| 5. سالها/تاریخها → حفظ شوند | |
| 6. کلمات عمومی → حفظ شوند | |
| 7. گروهها → company-XX | |
| **فقط متن ناشناسشده را برگردان - هیچ توضیح اضافی نیاز نیست.** | |
| """ | |
| 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=30 # کاهش از 60 به 30 ثانیه | |
| ) | |
| # بررسی خطای احراز هویت (401) | |
| if response.status_code == 401: | |
| raise Exception("❌ کلید API نامعتبر است! لطفاً کلید صحیح وارد کنید.") | |
| # بررسی خطای 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(5) # انتظار کمتر برای timeout | |
| except requests.exceptions.ConnectionError: | |
| last_error = f"❌ خطای اتصال به سرور Cerebras. اینترنت را بررسی کنید." | |
| time.sleep(5) | |
| 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) | |
| # اول پیام شروع را نشان بده | |
| yield { | |
| "type": "info", | |
| "message": f"🔄 در حال آمادهسازی برای پردازش {self.total_rows} ردیف...", | |
| "total": self.total_rows | |
| } | |
| # ایجاد anonymizer | |
| try: | |
| self.anonymizer = AdvancedCerebrasAnonymizer( | |
| api_key=self.api_key, | |
| rate_limit_config=self.rate_limit_config | |
| ) | |
| except Exception as e: | |
| yield { | |
| "type": "error", | |
| "message": f"❌ خطا در ایجاد Anonymizer: {str(e)}" | |
| } | |
| return | |
| # تست اتصال اولیه با متن کوتاه | |
| yield { | |
| "type": "info", | |
| "message": "🔄 در حال تست اتصال به API...", | |
| "total": self.total_rows | |
| } | |
| try: | |
| test_result = self.anonymizer.anonymize_text("تست: بانک ملی") | |
| if not test_result["success"]: | |
| yield { | |
| "type": "error", | |
| "message": f"❌ خطا در اتصال به API: {test_result.get('error', 'نامشخص')}" | |
| } | |
| return | |
| except Exception as e: | |
| yield { | |
| "type": "error", | |
| "message": f"❌ خطا در اتصال به API: {str(e)}" | |
| } | |
| return | |
| yield { | |
| "type": "info", | |
| "message": f"✅ اتصال برقرار! شروع پردازش {self.total_rows} ردیف...", | |
| "total": self.total_rows | |
| } | |
| # ایجاد ستون خروجی | |
| 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']}") | |
| # نمایش پیام info در UI | |
| yield ( | |
| 0, | |
| f"### ℹ️ {update['message']}", | |
| "", | |
| "\n".join(log_lines), | |
| None, | |
| gr.update(visible=False) | |
| ) | |
| 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 | |
| ) | |